diff --git a/src/System.Text.Json/ref/System.Text.Json.cs b/src/System.Text.Json/ref/System.Text.Json.cs index b95392a5e26a..25aa94e83bc6 100644 --- a/src/System.Text.Json/ref/System.Text.Json.cs +++ b/src/System.Text.Json/ref/System.Text.Json.cs @@ -49,6 +49,20 @@ public enum JsonTokenType : byte String = (byte)6, True = (byte)8, } + public partial struct JsonWriterOptions + { + private object _dummy; + public bool Indented { get { throw null; } set { } } + public bool SkipValidation { get { throw null; } set { } } + } + public partial struct JsonWriterState + { + private object _dummy; + public JsonWriterState(System.Text.Json.JsonWriterOptions options = default(System.Text.Json.JsonWriterOptions)) { throw null; } + public long BytesCommitted { get { throw null; } } + public long BytesWritten { get { throw null; } } + public System.Text.Json.JsonWriterOptions Options { get { throw null; } } + } public ref partial struct Utf8JsonReader { private object _dummy; @@ -71,4 +85,95 @@ public ref partial struct Utf8JsonReader public bool TryGetInt64Value(out long value) { throw null; } public bool TryGetSingleValue(out float value) { throw null; } } + public ref partial struct Utf8JsonWriter + { + private object _dummy; + public Utf8JsonWriter(System.Buffers.IBufferWriter bufferWriter, System.Text.Json.JsonWriterState state = default(System.Text.Json.JsonWriterState)) { throw null; } + public long BytesCommitted { get { throw null; } } + public long BytesWritten { get { throw null; } } + public int CurrentDepth { get { throw null; } } + public void Flush(bool isFinalBlock = true) { } + public System.Text.Json.JsonWriterState GetCurrentState() { throw null; } + public void WriteBoolean(System.ReadOnlySpan utf8PropertyName, bool value, bool escape = true) { } + public void WriteBoolean(System.ReadOnlySpan propertyName, bool value, bool escape = true) { } + public void WriteBoolean(string propertyName, bool value, bool escape = true) { } + public void WriteBooleanValue(bool value) { } + public void WriteCommentValue(System.ReadOnlySpan utf8Value, bool escape = true) { } + public void WriteCommentValue(System.ReadOnlySpan value, bool escape = true) { } + public void WriteCommentValue(string value, bool escape = true) { } + public void WriteEndArray() { } + public void WriteEndObject() { } + public void WriteNull(System.ReadOnlySpan utf8PropertyName, bool escape = true) { } + public void WriteNull(System.ReadOnlySpan propertyName, bool escape = true) { } + public void WriteNull(string propertyName, bool escape = true) { } + public void WriteNullValue() { } + public void WriteNumber(System.ReadOnlySpan utf8PropertyName, decimal value, bool escape = true) { } + public void WriteNumber(System.ReadOnlySpan utf8PropertyName, double value, bool escape = true) { } + public void WriteNumber(System.ReadOnlySpan utf8PropertyName, int value, bool escape = true) { } + public void WriteNumber(System.ReadOnlySpan utf8PropertyName, long value, bool escape = true) { } + public void WriteNumber(System.ReadOnlySpan utf8PropertyName, float value, bool escape = true) { } + [System.CLSCompliantAttribute(false)] + public void WriteNumber(System.ReadOnlySpan utf8PropertyName, uint value, bool escape = true) { } + [System.CLSCompliantAttribute(false)] + public void WriteNumber(System.ReadOnlySpan utf8PropertyName, ulong value, bool escape = true) { } + public void WriteNumber(System.ReadOnlySpan propertyName, decimal value, bool escape = true) { } + public void WriteNumber(System.ReadOnlySpan propertyName, double value, bool escape = true) { } + public void WriteNumber(System.ReadOnlySpan propertyName, int value, bool escape = true) { } + public void WriteNumber(System.ReadOnlySpan propertyName, long value, bool escape = true) { } + public void WriteNumber(System.ReadOnlySpan propertyName, float value, bool escape = true) { } + [System.CLSCompliantAttribute(false)] + public void WriteNumber(System.ReadOnlySpan propertyName, uint value, bool escape = true) { } + [System.CLSCompliantAttribute(false)] + public void WriteNumber(System.ReadOnlySpan propertyName, ulong value, bool escape = true) { } + public void WriteNumber(string propertyName, decimal value, bool escape = true) { } + public void WriteNumber(string propertyName, double value, bool escape = true) { } + public void WriteNumber(string propertyName, int value, bool escape = true) { } + public void WriteNumber(string propertyName, long value, bool escape = true) { } + public void WriteNumber(string propertyName, float value, bool escape = true) { } + [System.CLSCompliantAttribute(false)] + public void WriteNumber(string propertyName, uint value, bool escape = true) { } + [System.CLSCompliantAttribute(false)] + public void WriteNumber(string propertyName, ulong value, bool escape = true) { } + public void WriteNumberValue(decimal value) { } + public void WriteNumberValue(double value) { } + public void WriteNumberValue(int value) { } + public void WriteNumberValue(long value) { } + public void WriteNumberValue(float value) { } + [System.CLSCompliantAttribute(false)] + public void WriteNumberValue(uint value) { } + [System.CLSCompliantAttribute(false)] + public void WriteNumberValue(ulong value) { } + public void WriteStartArray() { } + public void WriteStartArray(System.ReadOnlySpan utf8PropertyName, bool escape = true) { } + public void WriteStartArray(System.ReadOnlySpan propertyName, bool escape = true) { } + public void WriteStartArray(string propertyName, bool escape = true) { } + public void WriteStartObject() { } + public void WriteStartObject(System.ReadOnlySpan utf8PropertyName, bool escape = true) { } + public void WriteStartObject(System.ReadOnlySpan propertyName, bool escape = true) { } + public void WriteStartObject(string propertyName, bool escape = true) { } + public void WriteString(System.ReadOnlySpan utf8PropertyName, System.DateTime value, bool escape = true) { } + public void WriteString(System.ReadOnlySpan utf8PropertyName, System.DateTimeOffset value, bool escape = true) { } + public void WriteString(System.ReadOnlySpan utf8PropertyName, System.Guid value, bool escape = true) { } + public void WriteString(System.ReadOnlySpan utf8PropertyName, System.ReadOnlySpan utf8Value, bool escape = true) { } + public void WriteString(System.ReadOnlySpan utf8PropertyName, System.ReadOnlySpan value, bool escape = true) { } + public void WriteString(System.ReadOnlySpan utf8PropertyName, string value, bool escape = true) { } + public void WriteString(System.ReadOnlySpan propertyName, System.DateTime value, bool escape = true) { } + public void WriteString(System.ReadOnlySpan propertyName, System.DateTimeOffset value, bool escape = true) { } + public void WriteString(System.ReadOnlySpan propertyName, System.Guid value, bool escape = true) { } + public void WriteString(System.ReadOnlySpan propertyName, System.ReadOnlySpan utf8Value, bool escape = true) { } + public void WriteString(System.ReadOnlySpan propertyName, System.ReadOnlySpan value, bool escape = true) { } + public void WriteString(System.ReadOnlySpan propertyName, string value, bool escape = true) { } + public void WriteString(string propertyName, System.DateTime value, bool escape = true) { } + public void WriteString(string propertyName, System.DateTimeOffset value, bool escape = true) { } + public void WriteString(string propertyName, System.Guid value, bool escape = true) { } + public void WriteString(string propertyName, System.ReadOnlySpan utf8Value, bool escape = true) { } + public void WriteString(string propertyName, System.ReadOnlySpan value, bool escape = true) { } + public void WriteString(string propertyName, string value, bool escape = true) { } + public void WriteStringValue(System.DateTime value) { } + public void WriteStringValue(System.DateTimeOffset value) { } + public void WriteStringValue(System.Guid value) { } + public void WriteStringValue(System.ReadOnlySpan utf8Value, bool escape = true) { } + public void WriteStringValue(System.ReadOnlySpan value, bool escape = true) { } + public void WriteStringValue(string value, bool escape = true) { } + } } diff --git a/src/System.Text.Json/src/Resources/Strings.resx b/src/System.Text.Json/src/Resources/Strings.resx index c1c47799045b..33d1d0f6a578 100644 --- a/src/System.Text.Json/src/Resources/Strings.resx +++ b/src/System.Text.Json/src/Resources/Strings.resx @@ -120,6 +120,36 @@ CurrentDepth ({0}) is larger than the maximum configured depth of {1}. Cannot read next JSON array. + + The JSON writer needs to be flushed before getting the current state. There are {0} bytes that have not been committed to the output. + + + Cannot write the start of an object/array after a single JSON value or outside of an existing closed object/array. Current token type is '{0}'. + + + Cannot write the start of an object or array without a property name. Current token type is '{0}'. + + + Cannot write invalid UTF-16 text as JSON. Invalid surrogate pair: '{0}'. + + + Cannot write invalid UTF-8 text as JSON. Invalid input: '{0}'. + + + Cannot write a JSON property within an array or as the first JSON token. Current token type is '{0}'. + + + Cannot write a JSON value after a single JSON value. Current token type is '{0}'. + + + Cannot write a JSON value within an object without a property name. Current token type is '{0}'. + + + CurrentDepth ({0}) is equal to or larger than the maximum allowed depth of {1}. Cannot write the next JSON object or array. + + + Writing an empty JSON payload (excluding comments) is invalid. + Expected end of comment, but instead reached end of data. @@ -159,6 +189,12 @@ Expected a value, but instead reached end of data. + + The 'IBufferWriter' could not provide an output buffer that is large enough to continue writing. + + + The 'IBufferWriter' could not provide an output buffer that is large enough to continue writing. Need at least {0} bytes. + '{0}' is invalid after a value. Expected either ',', '}}', or ']'. @@ -186,6 +222,9 @@ CurrentDepth ({0}) is larger than the maximum configured depth of {1}. Cannot read next JSON object. + + The JSON property name of length {0} is too large and not supported by the JSON writer. + '{0}' is invalid within a number, immediately after a decimal point ('.'). Expected a digit ('0'-'9'). @@ -195,6 +234,12 @@ Expected a digit ('0'-'9'), but instead reached end of data. + + .NET number values such as positive and negative infinity cannot be written as valid JSON. + + + The JSON value of length {0} is too large and not supported by the JSON writer. + Expected CurrentDepth ({0}) to be zero at the end of the JSON payload. There is an open JSON object or array that should be closed. diff --git a/src/System.Text.Json/src/System.Text.Json.csproj b/src/System.Text.Json/src/System.Text.Json.csproj index a158dabc85f3..6d774c2bcc32 100644 --- a/src/System.Text.Json/src/System.Text.Json.csproj +++ b/src/System.Text.Json/src/System.Text.Json.csproj @@ -8,19 +8,49 @@ - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -30,6 +60,7 @@ + diff --git a/src/System.Text.Json/src/System/Text/Json/JsonConstants.cs b/src/System.Text.Json/src/System/Text/Json/JsonConstants.cs index 859d70611f90..c56f8d91d15c 100644 --- a/src/System.Text.Json/src/System/Text/Json/JsonConstants.cs +++ b/src/System.Text.Json/src/System/Text/Json/JsonConstants.cs @@ -14,7 +14,7 @@ internal static class JsonConstants public const byte CarriageReturn = (byte)'\r'; public const byte LineFeed = (byte)'\n'; public const byte Tab = (byte)'\t'; - public const byte ListSeperator = (byte)','; + public const byte ListSeparator = (byte)','; public const byte KeyValueSeperator = (byte)':'; public const byte Quote = (byte)'"'; public const byte BackSlash = (byte)'\\'; @@ -28,13 +28,44 @@ internal static class JsonConstants public static ReadOnlySpan NullValue => new byte[] { (byte)'n', (byte)'u', (byte)'l', (byte)'l' }; // Used to search for the end of a number - public static ReadOnlySpan Delimiters => new byte[] { ListSeperator, CloseBrace, CloseBracket, Space, LineFeed, CarriageReturn, Tab, Slash }; - - public static ReadOnlySpan WhiteSpace => new byte[] { Space, LineFeed, CarriageReturn, Tab }; - - public static ReadOnlySpan EndOfComment => new byte[] { Asterisk, Slash }; + public static ReadOnlySpan Delimiters => new byte[] { ListSeparator, CloseBrace, CloseBracket, Space, LineFeed, CarriageReturn, Tab, Slash }; // Explicitly skipping ReverseSolidus since that is handled separately public static ReadOnlySpan EscapableChars => new byte[] { Quote, (byte)'n', (byte)'r', (byte)'t', Slash, (byte)'u', (byte)'b', (byte)'f' }; + + public const int SpacesPerIndent = 2; + public const int MaxWriterDepth = 1_000; + public const int RemoveFlagsBitMask = 0x7FFFFFFF; + + // In the worst case, an ASCII character represented as a single utf-8 byte could expand 6x when escaped. + // For example: '+' becomes '\u0043' + // Escaping surrogate pairs (represented by 3 or 4 utf-8 bytes) would expand to 12 bytes (which is still <= 6x). + // The same factor applies to utf-16 characters. + public const int MaxExpansionFactorWhileEscaping = 6; + + public const int MaxTokenSize = 2_000_000_000 / MaxExpansionFactorWhileEscaping; // 357_913_941 bytes + public const int MaxCharacterTokenSize = 2_000_000_000 / MaxExpansionFactorWhileEscaping; // 357_913_941 characters + + public const int MaximumFormatInt64Length = 20; // 19 + sign (i.e. -9223372036854775808) + public const int MaximumFormatUInt64Length = 20; // i.e. 18446744073709551615 + public const int MaximumFormatDoubleLength = 128; // default (i.e. 'G'), using 128 (rather than say 32) to be future-proof. + public const int MaximumFormatSingleLength = 128; // default (i.e. 'G'), using 128 (rather than say 32) to be future-proof. + public const int MaximumFormatDecimalLength = 31; // default (i.e. 'G') + public const int MaximumFormatGuidLength = 36; // default (i.e. 'D'), 8 + 4 + 4 + 4 + 12 + 4 for the hyphens (e.g. 094ffa0a-0442-494d-b452-04003fa755cc) + public const int MaximumFormatDateTimeLength = 27; // StandardFormat 'O', e.g. 2017-06-12T05:30:45.7680000 + public const int MaximumFormatDateTimeOffsetLength = 33; // StandardFormat 'O', e.g. 2017-06-12T05:30:45.7680000-07:00 + + // Encoding Helpers + public const char HighSurrogateStart = '\ud800'; + public const char HighSurrogateEnd = '\udbff'; + public const char LowSurrogateStart = '\udc00'; + public const char LowSurrogateEnd = '\udfff'; + + public const int UnicodePlane01StartValue = 0x10000; + public const int HighSurrogateStartValue = 0xD800; + public const int HighSurrogateEndValue = 0xDBFF; + public const int LowSurrogateStartValue = 0xDC00; + public const int LowSurrogateEndValue = 0xDFFF; + public const int ShiftRightBy10 = 0x400; } } diff --git a/src/System.Text.Json/src/System/Text/Json/ConsumeNumberResult.cs b/src/System.Text.Json/src/System/Text/Json/Reader/ConsumeNumberResult.cs similarity index 100% rename from src/System.Text.Json/src/System/Text/Json/ConsumeNumberResult.cs rename to src/System.Text.Json/src/System/Text/Json/Reader/ConsumeNumberResult.cs diff --git a/src/System.Text.Json/src/System/Text/Json/ConsumeTokenResult.cs b/src/System.Text.Json/src/System/Text/Json/Reader/ConsumeTokenResult.cs similarity index 100% rename from src/System.Text.Json/src/System/Text/Json/ConsumeTokenResult.cs rename to src/System.Text.Json/src/System/Text/Json/Reader/ConsumeTokenResult.cs diff --git a/src/System.Text.Json/src/System/Text/Json/JsonReaderException.cs b/src/System.Text.Json/src/System/Text/Json/Reader/JsonReaderException.cs similarity index 100% rename from src/System.Text.Json/src/System/Text/Json/JsonReaderException.cs rename to src/System.Text.Json/src/System/Text/Json/Reader/JsonReaderException.cs diff --git a/src/System.Text.Json/src/System/Text/Json/JsonReaderHelper.cs b/src/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.cs similarity index 100% rename from src/System.Text.Json/src/System/Text/Json/JsonReaderHelper.cs rename to src/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.cs diff --git a/src/System.Text.Json/src/System/Text/Json/JsonReaderOptions.cs b/src/System.Text.Json/src/System/Text/Json/Reader/JsonReaderOptions.cs similarity index 100% rename from src/System.Text.Json/src/System/Text/Json/JsonReaderOptions.cs rename to src/System.Text.Json/src/System/Text/Json/Reader/JsonReaderOptions.cs diff --git a/src/System.Text.Json/src/System/Text/Json/JsonReaderState.cs b/src/System.Text.Json/src/System/Text/Json/Reader/JsonReaderState.cs similarity index 100% rename from src/System.Text.Json/src/System/Text/Json/JsonReaderState.cs rename to src/System.Text.Json/src/System/Text/Json/Reader/JsonReaderState.cs diff --git a/src/System.Text.Json/src/System/Text/Json/Utf8JsonReader.MultiSegment.cs b/src/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.MultiSegment.cs similarity index 99% rename from src/System.Text.Json/src/System/Text/Json/Utf8JsonReader.MultiSegment.cs rename to src/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.MultiSegment.cs index 58eea7fdb7de..51f8c786e6ff 100644 --- a/src/System.Text.Json/src/System/Text/Json/Utf8JsonReader.MultiSegment.cs +++ b/src/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.MultiSegment.cs @@ -1560,7 +1560,7 @@ private ConsumeTokenResult ConsumeNextTokenMultiSegment(byte marker) ThrowHelper.ThrowJsonReaderException(ref this, ExceptionResource.ExpectedEndAfterSingleJson, marker); } - if (marker == JsonConstants.ListSeperator) + if (marker == JsonConstants.ListSeparator) { _consumed++; _bytePositionInLine++; @@ -1669,7 +1669,7 @@ private ConsumeTokenResult ConsumeNextTokenFromLastNonCommentTokenMultiSegment() Debug.Assert(first != JsonConstants.Slash); - if (first == JsonConstants.ListSeperator) + if (first == JsonConstants.ListSeparator) { _consumed++; _bytePositionInLine++; @@ -1954,7 +1954,7 @@ private ConsumeTokenResult ConsumeNextTokenUntilAfterAllCommentsAreSkippedMultiS { ThrowHelper.ThrowJsonReaderException(ref this, ExceptionResource.ExpectedEndAfterSingleJson, marker); } - else if (marker == JsonConstants.ListSeperator) + else if (marker == JsonConstants.ListSeparator) { _consumed++; _bytePositionInLine++; diff --git a/src/System.Text.Json/src/System/Text/Json/Utf8JsonReader.TryGet.cs b/src/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs similarity index 100% rename from src/System.Text.Json/src/System/Text/Json/Utf8JsonReader.TryGet.cs rename to src/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs diff --git a/src/System.Text.Json/src/System/Text/Json/Utf8JsonReader.cs b/src/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.cs similarity index 99% rename from src/System.Text.Json/src/System/Text/Json/Utf8JsonReader.cs rename to src/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.cs index 708f3e684488..0b60c3f902ac 100644 --- a/src/System.Text.Json/src/System/Text/Json/Utf8JsonReader.cs +++ b/src/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.cs @@ -1221,7 +1221,7 @@ private ConsumeTokenResult ConsumeNextToken(byte marker) ThrowHelper.ThrowJsonReaderException(ref this, ExceptionResource.ExpectedEndAfterSingleJson, marker); } - if (marker == JsonConstants.ListSeperator) + if (marker == JsonConstants.ListSeparator) { _consumed++; _bytePositionInLine++; @@ -1321,7 +1321,7 @@ private ConsumeTokenResult ConsumeNextTokenFromLastNonCommentToken() Debug.Assert(first != JsonConstants.Slash); - if (first == JsonConstants.ListSeperator) + if (first == JsonConstants.ListSeparator) { _consumed++; _bytePositionInLine++; @@ -1593,7 +1593,7 @@ private ConsumeTokenResult ConsumeNextTokenUntilAfterAllCommentsAreSkipped(byte { ThrowHelper.ThrowJsonReaderException(ref this, ExceptionResource.ExpectedEndAfterSingleJson, marker); } - else if (marker == JsonConstants.ListSeperator) + else if (marker == JsonConstants.ListSeparator) { _consumed++; _bytePositionInLine++; diff --git a/src/System.Text.Json/src/System/Text/Json/ThrowHelper.cs b/src/System.Text.Json/src/System/Text/Json/ThrowHelper.cs index 14e492732b23..4831b531541e 100644 --- a/src/System.Text.Json/src/System/Text/Json/ThrowHelper.cs +++ b/src/System.Text.Json/src/System/Text/Json/ThrowHelper.cs @@ -20,6 +20,154 @@ private static ArgumentException GetArgumentException(string message) return new ArgumentException(message); } + public static void ThrowArgumentException(string message) + { + throw GetArgumentException(message); + } + + public static InvalidOperationException GetInvalidOperationException_CallFlushFirst(int _buffered) + { + return new InvalidOperationException(SR.Format(SR.CallFlushToAvoidDataLoss, _buffered)); + } + + public static void ThrowArgumentException_PropertyNameTooLarge(int tokenLength) + { + throw GetArgumentException(SR.Format(SR.PropertyNameTooLarge, tokenLength)); + } + + public static void ThrowArgumentException_ValueTooLarge(int tokenLength) + { + throw GetArgumentException(SR.Format(SR.ValueTooLarge, tokenLength)); + } + + public static void ThrowArgumentException_ValueNotSupported() + { + throw GetArgumentException(SR.SpecialNumberValuesNotSupported); + } + + public static void ThrowArgumentException(ExceptionResource resource, int minimumSize = 0) + { + if (resource == ExceptionResource.FailedToGetLargerSpan) + { + throw GetArgumentException(SR.FailedToGetLargerSpan); + } + else + { + Debug.Assert(resource == ExceptionResource.FailedToGetMinimumSizeSpan); + throw GetArgumentException(SR.Format(SR.FailedToGetMinimumSizeSpan, minimumSize)); + } + } + + public static void ThrowArgumentException(ReadOnlySpan propertyName, ReadOnlySpan value) + { + if (propertyName.Length > JsonConstants.MaxTokenSize) + { + ThrowArgumentException(SR.Format(SR.PropertyNameTooLarge, propertyName.Length)); + } + else + { + Debug.Assert(value.Length > JsonConstants.MaxTokenSize); + ThrowArgumentException(SR.Format(SR.ValueTooLarge, value.Length)); + } + } + + public static void ThrowArgumentException(ReadOnlySpan propertyName, ReadOnlySpan value) + { + if (propertyName.Length > JsonConstants.MaxTokenSize) + { + ThrowArgumentException(SR.Format(SR.PropertyNameTooLarge, propertyName.Length)); + } + else + { + Debug.Assert(value.Length > JsonConstants.MaxCharacterTokenSize); + ThrowArgumentException(SR.Format(SR.ValueTooLarge, value.Length)); + } + } + + public static void ThrowArgumentException(ReadOnlySpan propertyName, ReadOnlySpan value) + { + if (propertyName.Length > JsonConstants.MaxCharacterTokenSize) + { + ThrowArgumentException(SR.Format(SR.PropertyNameTooLarge, propertyName.Length)); + } + else + { + Debug.Assert(value.Length > JsonConstants.MaxTokenSize); + ThrowArgumentException(SR.Format(SR.ValueTooLarge, value.Length)); + } + } + + public static void ThrowArgumentException(ReadOnlySpan propertyName, ReadOnlySpan value) + { + if (propertyName.Length > JsonConstants.MaxCharacterTokenSize) + { + ThrowArgumentException(SR.Format(SR.PropertyNameTooLarge, propertyName.Length)); + } + else + { + Debug.Assert(value.Length > JsonConstants.MaxCharacterTokenSize); + ThrowArgumentException(SR.Format(SR.ValueTooLarge, value.Length)); + } + } + + public static void ThrowInvalidOperationOrArgumentException(ReadOnlySpan propertyName, int currentDepth) + { + currentDepth &= JsonConstants.RemoveFlagsBitMask; + if (currentDepth >= JsonConstants.MaxWriterDepth) + { + ThrowInvalidOperationException(SR.Format(SR.DepthTooLarge, currentDepth, JsonConstants.MaxWriterDepth)); + } + else + { + Debug.Assert(propertyName.Length > JsonConstants.MaxCharacterTokenSize); + ThrowArgumentException(SR.Format(SR.PropertyNameTooLarge, propertyName.Length)); + } + } + + public static void ThrowInvalidOperationException(string message) + { + throw GetInvalidOperationException(message); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static InvalidOperationException GetInvalidOperationException(string message) + { + return new InvalidOperationException(message); + } + + public static void ThrowInvalidOperationException_DepthNonZeroOrEmptyJson(int currentDepth) + { + throw GetInvalidOperationException(currentDepth); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static InvalidOperationException GetInvalidOperationException(int currentDepth) + { + currentDepth &= JsonConstants.RemoveFlagsBitMask; + if (currentDepth != 0) + { + return new InvalidOperationException(SR.Format(SR.ZeroDepthAtEnd, currentDepth)); + } + else + { + return new InvalidOperationException(SR.Format(SR.EmptyJsonIsInvalid)); + } + } + + public static void ThrowInvalidOperationOrArgumentException(ReadOnlySpan propertyName, int currentDepth) + { + currentDepth &= JsonConstants.RemoveFlagsBitMask; + if (currentDepth >= JsonConstants.MaxWriterDepth) + { + ThrowInvalidOperationException(SR.Format(SR.DepthTooLarge, currentDepth, JsonConstants.MaxWriterDepth)); + } + else + { + Debug.Assert(propertyName.Length > JsonConstants.MaxCharacterTokenSize); + ThrowArgumentException(SR.Format(SR.PropertyNameTooLarge, propertyName.Length)); + } + } + public static InvalidOperationException GetInvalidOperationException_ExpectedNumber(JsonTokenType tokenType) { return GetInvalidOperationException("number", tokenType); @@ -151,6 +299,86 @@ private static string GetResourceString(ref Utf8JsonReader json, ExceptionResour return message; } + + public static void ThrowInvalidOperationException(ExceptionResource resource, int currentDepth = default, byte token = default, JsonTokenType tokenType = default) + { + throw GetInvalidOperationException(resource, currentDepth, token, tokenType); + } + + public static void ThrowArgumentException_InvalidUTF8(ReadOnlySpan value) + { + var builder = new StringBuilder(); + + int printFirst10 = Math.Min(value.Length, 10); + + for (int i = 0; i < printFirst10; i++) + { + byte nextByte = value[i]; + if (IsPrintable(nextByte)) + { + builder.Append((char)nextByte); + } + else + { + builder.Append($"0x{nextByte:X2}"); + } + } + + if (printFirst10 < value.Length) + { + builder.Append("..."); + } + + throw new ArgumentException(SR.Format(SR.CannotWriteInvalidUTF8, builder.ToString())); + } + + public static void ThrowArgumentException_InvalidUTF16(int charAsInt) + { + throw new ArgumentException(SR.Format(SR.CannotWriteInvalidUTF16, $"0x{charAsInt:X2}")); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static InvalidOperationException GetInvalidOperationException(ExceptionResource resource, int currentDepth, byte token, JsonTokenType tokenType) + { + string message = GetResourceString(resource, currentDepth, token, tokenType); + return new InvalidOperationException(message); + } + + // This function will convert an ExceptionResource enum value to the resource string. + [MethodImpl(MethodImplOptions.NoInlining)] + private static string GetResourceString(ExceptionResource resource, int currentDepth, byte token, JsonTokenType tokenType) + { + string message = ""; + switch (resource) + { + case ExceptionResource.MismatchedObjectArray: + message = SR.Format(SR.MismatchedObjectArray, token); + break; + case ExceptionResource.DepthTooLarge: + message = SR.Format(SR.DepthTooLarge, currentDepth & JsonConstants.RemoveFlagsBitMask, JsonConstants.MaxWriterDepth); + break; + case ExceptionResource.CannotStartObjectArrayWithoutProperty: + message = SR.Format(SR.CannotStartObjectArrayWithoutProperty, tokenType); + break; + case ExceptionResource.CannotStartObjectArrayAfterPrimitiveOrClose: + message = SR.Format(SR.CannotStartObjectArrayAfterPrimitiveOrClose, tokenType); + break; + case ExceptionResource.CannotWriteValueWithinObject: + message = SR.Format(SR.CannotWriteValueWithinObject, tokenType); + break; + case ExceptionResource.CannotWritePropertyWithinArray: + message = SR.Format(SR.CannotWritePropertyWithinArray, tokenType); + break; + case ExceptionResource.CannotWriteValueAfterPrimitive: + message = SR.Format(SR.CannotWriteValueAfterPrimitive, tokenType); + break; + default: + Debug.Fail($"The ExceptionResource enum value: {resource} is not part of the switch. Add the appropriate case and exception message."); + break; + } + + return message; + } } internal enum ExceptionResource @@ -180,5 +408,13 @@ internal enum ExceptionResource MismatchedObjectArray, ObjectDepthTooLarge, ZeroDepthAtEnd, + DepthTooLarge, + CannotStartObjectArrayWithoutProperty, + CannotStartObjectArrayAfterPrimitiveOrClose, + CannotWriteValueWithinObject, + CannotWriteValueAfterPrimitive, + FailedToGetMinimumSizeSpan, + FailedToGetLargerSpan, + CannotWritePropertyWithinArray, } } diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/JsonWriterHelper.Escaping.cs b/src/System.Text.Json/src/System/Text/Json/Writer/JsonWriterHelper.Escaping.cs new file mode 100644 index 000000000000..18e946272e7a --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/JsonWriterHelper.Escaping.cs @@ -0,0 +1,475 @@ +// 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.Buffers.Text; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace System.Text.Json +{ + // TODO: Replace the escaping logic with publicly shipping APIs from https://github.com/dotnet/corefx/issues/33509 + internal static partial class JsonWriterHelper + { + // Only allow ASCII characters between ' ' (0x20) and '~' (0x7E), inclusively, + // but exclude characters that need to be escaped as hex: '"', '\'', '&', '+', '<', '>', '`' + // and exclude characters that need to be escaped by adding a backslash: '\n', '\r', '\t', '\\', '/', '\b', '\f' + // + // non-zero = allowed, 0 = disallowed + private static ReadOnlySpan AllowList => new byte[byte.MaxValue + 1] { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + + private static readonly char[] s_hexFormat = { 'x', '4' }; + private static readonly StandardFormat s_hexStandardFormat = new StandardFormat('x', 4); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool NeedsEscaping(byte value) => AllowList[value] == 0; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool NeedsEscaping(char value) => value > byte.MaxValue || AllowList[value] == 0; + + public static int NeedsEscaping(ReadOnlySpan value) + { + int idx; + for (idx = 0; idx < value.Length; idx++) + { + if (NeedsEscaping(value[idx])) + { + goto Return; + } + } + + idx = -1; // all characters allowed + + Return: + return idx; + } + + public static int NeedsEscaping(ReadOnlySpan value) + { + int idx; + for (idx = 0; idx < value.Length; idx++) + { + if (NeedsEscaping(value[idx])) + { + goto Return; + } + } + + idx = -1; // all characters allowed + + Return: + return idx; + } + + public static int GetMaxEscapedLength(int textLength, int firstIndexToEscape) + { + Debug.Assert(textLength > 0); + Debug.Assert(firstIndexToEscape >= 0 && firstIndexToEscape < textLength); + return firstIndexToEscape + JsonConstants.MaxExpansionFactorWhileEscaping * (textLength - firstIndexToEscape); + } + + public static void EscapeString(ReadOnlySpan value, Span destination, int indexOfFirstByteToEscape, out int written) + { + Debug.Assert(indexOfFirstByteToEscape >= 0 && indexOfFirstByteToEscape < value.Length); + + value.Slice(0, indexOfFirstByteToEscape).CopyTo(destination); + written = indexOfFirstByteToEscape; + int consumed = indexOfFirstByteToEscape; + + while (consumed < value.Length) + { + byte val = value[consumed]; + if (NeedsEscaping(val)) + { + consumed += EscapeNextBytes(value.Slice(consumed), destination, ref written); + } + else + { + destination[written] = val; + written++; + consumed++; + } + } + } + + private static int EscapeNextBytes(ReadOnlySpan value, Span destination, ref int written) + { + SequenceValidity status = PeekFirstSequence(value, out int numBytesConsumed, out Rune rune); + if (status != SequenceValidity.WellFormed) + ThrowHelper.ThrowArgumentException_InvalidUTF8(value); + + destination[written++] = (byte)'\\'; + int scalar = rune.Value; + switch (scalar) + { + case JsonConstants.LineFeed: + destination[written++] = (byte)'n'; + break; + case JsonConstants.CarriageReturn: + destination[written++] = (byte)'r'; + break; + case JsonConstants.Tab: + destination[written++] = (byte)'t'; + break; + case JsonConstants.BackSlash: + destination[written++] = (byte)'\\'; + break; + case JsonConstants.BackSpace: + destination[written++] = (byte)'b'; + break; + case JsonConstants.FormFeed: + destination[written++] = (byte)'f'; + break; + default: + destination[written++] = (byte)'u'; + if (scalar < JsonConstants.UnicodePlane01StartValue) + { + bool result = Utf8Formatter.TryFormat(scalar, destination.Slice(written), out int bytesWritten, format: s_hexStandardFormat); + Debug.Assert(result); + Debug.Assert(bytesWritten == 4); + written += bytesWritten; + } + else + { + // Divide by 0x400 to shift right by 10 in order to find the surrogate pairs from the scalar + // High surrogate = ((scalar - 0x10000) / 0x400) + D800 + // Low surrogate = ((scalar - 0x10000) % 0x400) + DC00 + int quotient = Math.DivRem(scalar - JsonConstants.UnicodePlane01StartValue, JsonConstants.ShiftRightBy10, out int remainder); + int firstChar = quotient + JsonConstants.HighSurrogateStartValue; + int nextChar = remainder + JsonConstants.LowSurrogateStartValue; + bool result = Utf8Formatter.TryFormat(firstChar, destination.Slice(written), out int bytesWritten, format: s_hexStandardFormat); + Debug.Assert(result); + Debug.Assert(bytesWritten == 4); + written += bytesWritten; + destination[written++] = (byte)'\\'; + destination[written++] = (byte)'u'; + result = Utf8Formatter.TryFormat(nextChar, destination.Slice(written), out bytesWritten, format: s_hexStandardFormat); + Debug.Assert(result); + Debug.Assert(bytesWritten == 4); + written += bytesWritten; + } + break; + } + return numBytesConsumed; + } + + private static bool IsAsciiValue(byte value) => value < 0x80; + + /// + /// Returns iff is a UTF-8 continuation byte. + /// A UTF-8 continuation byte is a byte whose value is in the range 0x80-0xBF, inclusive. + /// + private static bool IsUtf8ContinuationByte(byte value) => (value & 0xC0) == 0x80; + + /// + /// Returns iff is between + /// and , inclusive. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsInRangeInclusive(byte value, byte lowerBound, byte upperBound) + => ((byte)(value - lowerBound) <= (byte)(upperBound - lowerBound)); + + /// + /// Returns iff is between + /// and , inclusive. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsInRangeInclusive(uint value, uint lowerBound, uint upperBound) + => (value - lowerBound) <= (upperBound - lowerBound); + + /// + /// Returns iff the low word of is a UTF-16 surrogate. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsLowWordSurrogate(uint @char) + => (@char & 0xF800U) == 0xD800U; + + public static SequenceValidity PeekFirstSequence(ReadOnlySpan data, out int numBytesConsumed, out Rune rune) + { + // This method is implemented to match the behavior of System.Text.Encoding.UTF8 in terms of + // how many bytes it consumes when reporting invalid sequences. The behavior is as follows: + // + // - Some bytes are *always* invalid (ranges [ C0..C1 ] and [ F5..FF ]), and when these + // are encountered it's an invalid sequence of length 1. + // + // - Multi-byte sequences which are overlong are reported as an invalid sequence of length 2, + // since per the Unicode Standard Table 3-7 it's always possible to tell these by the second byte. + // Exception: Sequences which begin with [ C0..C1 ] are covered by the above case, thus length 1. + // + // - Multi-byte sequences which are improperly terminated (no continuation byte when one is + // expected) are reported as invalid sequences up to and including the last seen continuation byte. + + rune = Rune.ReplacementChar; + + if (data.IsEmpty) + { + // No data to peek at + numBytesConsumed = 0; + return SequenceValidity.Empty; + } + + byte firstByte = data[0]; + + if (IsAsciiValue(firstByte)) + { + // ASCII byte = well-formed one-byte sequence. + rune = new Rune(firstByte); + numBytesConsumed = 1; + return SequenceValidity.WellFormed; + } + + if (!IsInRangeInclusive(firstByte, (byte)0xC2U, (byte)0xF4U)) + { + // Standalone continuation byte or "always invalid" byte = ill-formed one-byte sequence. + goto InvalidOneByteSequence; + } + + // At this point, we know we're working with a multi-byte sequence, + // and we know that at least the first byte is potentially valid. + + if (data.Length < 2) + { + // One byte of an incomplete multi-byte sequence. + goto OneByteOfIncompleteMultiByteSequence; + } + + byte secondByte = data[1]; + + if (!IsUtf8ContinuationByte(secondByte)) + { + // One byte of an improperly terminated multi-byte sequence. + goto InvalidOneByteSequence; + } + + if (firstByte < (byte)0xE0U) + { + // Well-formed two-byte sequence. + rune = new Rune((((uint)firstByte & 0x1FU) << 6) | ((uint)secondByte & 0x3FU)); + numBytesConsumed = 2; + return SequenceValidity.WellFormed; + } + + if (firstByte < (byte)0xF0U) + { + // Start of a three-byte sequence. + // Need to check for overlong or surrogate sequences. + + uint scalar = (((uint)firstByte & 0x0FU) << 12) | (((uint)secondByte & 0x3FU) << 6); + if (scalar < 0x800U || IsLowWordSurrogate(scalar)) + { + goto OverlongOutOfRangeOrSurrogateSequence; + } + + // At this point, we have a valid two-byte start of a three-byte sequence. + + if (data.Length < 3) + { + // Two bytes of an incomplete three-byte sequence. + goto TwoBytesOfIncompleteMultiByteSequence; + } + else + { + byte thirdByte = data[2]; + if (IsUtf8ContinuationByte(thirdByte)) + { + // Well-formed three-byte sequence. + scalar |= (uint)thirdByte & 0x3FU; + rune = new Rune(scalar); + numBytesConsumed = 3; + return SequenceValidity.WellFormed; + } + else + { + // Two bytes of improperly terminated multi-byte sequence. + goto InvalidTwoByteSequence; + } + } + } + + { + // Start of four-byte sequence. + // Need to check for overlong or out-of-range sequences. + + uint scalar = (((uint)firstByte & 0x07U) << 18) | (((uint)secondByte & 0x3FU) << 12); + if (!IsInRangeInclusive(scalar, 0x10000U, 0x10FFFFU)) + { + goto OverlongOutOfRangeOrSurrogateSequence; + } + + // At this point, we have a valid two-byte start of a four-byte sequence. + + if (data.Length < 3) + { + // Two bytes of an incomplete four-byte sequence. + goto TwoBytesOfIncompleteMultiByteSequence; + } + else + { + byte thirdByte = data[2]; + if (IsUtf8ContinuationByte(thirdByte)) + { + // Valid three-byte start of a four-byte sequence. + + if (data.Length < 4) + { + // Three bytes of an incomplete four-byte sequence. + goto ThreeBytesOfIncompleteMultiByteSequence; + } + else + { + byte fourthByte = data[3]; + if (IsUtf8ContinuationByte(fourthByte)) + { + // Well-formed four-byte sequence. + scalar |= (((uint)thirdByte & 0x3FU) << 6) | ((uint)fourthByte & 0x3FU); + rune = new Rune(scalar); + numBytesConsumed = 4; + return SequenceValidity.WellFormed; + } + else + { + // Three bytes of an improperly terminated multi-byte sequence. + goto InvalidThreeByteSequence; + } + } + } + else + { + // Two bytes of improperly terminated multi-byte sequence. + goto InvalidTwoByteSequence; + } + } + } + + // Everything below here is error handling. + + InvalidOneByteSequence: + numBytesConsumed = 1; + return SequenceValidity.Invalid; + + InvalidTwoByteSequence: + OverlongOutOfRangeOrSurrogateSequence: + numBytesConsumed = 2; + return SequenceValidity.Invalid; + + InvalidThreeByteSequence: + numBytesConsumed = 3; + return SequenceValidity.Invalid; + + OneByteOfIncompleteMultiByteSequence: + numBytesConsumed = 1; + return SequenceValidity.Incomplete; + + TwoBytesOfIncompleteMultiByteSequence: + numBytesConsumed = 2; + return SequenceValidity.Incomplete; + + ThreeBytesOfIncompleteMultiByteSequence: + numBytesConsumed = 3; + return SequenceValidity.Incomplete; + } + + public static void EscapeString(ReadOnlySpan value, Span destination, int indexOfFirstByteToEscape, out int written) + { + Debug.Assert(indexOfFirstByteToEscape >= 0 && indexOfFirstByteToEscape < value.Length); + + value.Slice(0, indexOfFirstByteToEscape).CopyTo(destination); + written = indexOfFirstByteToEscape; + int consumed = indexOfFirstByteToEscape; + + while (consumed < value.Length) + { + char val = value[consumed]; + if (NeedsEscaping(val)) + { + EscapeNextChars(value, val, destination, ref consumed, ref written); + } + else + { + destination[written++] = val; + } + consumed++; + } + } + + private static void EscapeNextChars(ReadOnlySpan value, int firstChar, Span destination, ref int consumed, ref int written) + { + int nextChar = -1; + if (IsInRangeInclusive(firstChar, JsonConstants.HighSurrogateStartValue, JsonConstants.LowSurrogateEndValue)) + { + consumed++; + if (value.Length <= consumed || firstChar >= JsonConstants.LowSurrogateStartValue) + { + ThrowHelper.ThrowArgumentException_InvalidUTF16(firstChar); + } + + nextChar = value[consumed]; + if (!IsInRangeInclusive(nextChar, JsonConstants.LowSurrogateStartValue, JsonConstants.LowSurrogateEndValue)) + { + ThrowHelper.ThrowArgumentException_InvalidUTF16(nextChar); + } + } + + destination[written++] = '\\'; + switch (firstChar) + { + case JsonConstants.LineFeed: + destination[written++] = 'n'; + break; + case JsonConstants.CarriageReturn: + destination[written++] = 'r'; + break; + case JsonConstants.Tab: + destination[written++] = 't'; + break; + case JsonConstants.BackSlash: + destination[written++] = '\\'; + break; + case JsonConstants.BackSpace: + destination[written++] = 'b'; + break; + case JsonConstants.FormFeed: + destination[written++] = 'f'; + break; + default: + destination[written++] = 'u'; + firstChar.TryFormat(destination.Slice(written), out int charsWritten, s_hexFormat); + Debug.Assert(charsWritten == 4); + written += charsWritten; + if (nextChar != -1) + { + destination[written++] = '\\'; + destination[written++] = 'u'; + nextChar.TryFormat(destination.Slice(written), out charsWritten, s_hexFormat); + Debug.Assert(charsWritten == 4); + written += charsWritten; + } + break; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsInRangeInclusive(int ch, int start, int end) + { + return (uint)(ch - start) <= (uint)(end - start); + } + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/JsonWriterHelper.Transcoding.cs b/src/System.Text.Json/src/System/Text/Json/Writer/JsonWriterHelper.Transcoding.cs new file mode 100644 index 000000000000..9285ee07a5be --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/JsonWriterHelper.Transcoding.cs @@ -0,0 +1,334 @@ +// 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.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace System.Text.Json +{ + internal static partial class JsonWriterHelper + { + // TODO: Replace this with publicly shipping implementation: https://github.com/dotnet/corefx/issues/34094 + /// + /// Converts a span containing a sequence of UTF-16 bytes into UTF-8 bytes. + /// + /// This method will consume as many of the input bytes as possible. + /// + /// On successful exit, the entire input was consumed and encoded successfully. In this case, will be + /// equal to the length of the and will equal the total number of bytes written to + /// the . + /// + /// A span containing a sequence of UTF-16 bytes. + /// A span to write the UTF-8 bytes into. + /// On exit, contains the number of bytes that were consumed from the . + /// On exit, contains the number of bytes written to + /// A value representing the state of the conversion. + public unsafe static OperationStatus ToUtf8(ReadOnlySpan utf16Source, Span utf8Destination, out int bytesConsumed, out int bytesWritten) + { + // + // + // KEEP THIS IMPLEMENTATION IN SYNC WITH https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Text/UTF8Encoding.cs#L841 + // + // + fixed (byte* chars = &MemoryMarshal.GetReference(utf16Source)) + fixed (byte* bytes = &MemoryMarshal.GetReference(utf8Destination)) + { + char* pSrc = (char*)chars; + byte* pTarget = bytes; + + char* pEnd = (char*)(chars + utf16Source.Length); + byte* pAllocatedBufferEnd = pTarget + utf8Destination.Length; + + // assume that JIT will enregister pSrc, pTarget and ch + + // Entering the fast encoding loop incurs some overhead that does not get amortized for small + // number of characters, and the slow encoding loop typically ends up running for the last few + // characters anyway since the fast encoding loop needs 5 characters on input at least. + // Thus don't use the fast decoding loop at all if we don't have enough characters. The threashold + // was choosen based on performance testing. + // Note that if we don't have enough bytes, pStop will prevent us from entering the fast loop. + while (pEnd - pSrc > 13) + { + // we need at least 1 byte per character, but Convert might allow us to convert + // only part of the input, so try as much as we can. Reduce charCount if necessary + int available = Math.Min(PtrDiff(pEnd, pSrc), PtrDiff(pAllocatedBufferEnd, pTarget)); + + // FASTLOOP: + // - optimistic range checks + // - fallbacks to the slow loop for all special cases, exception throwing, etc. + + // To compute the upper bound, assume that all characters are ASCII characters at this point, + // the boundary will be decreased for every non-ASCII character we encounter + // Also, we need 5 chars reserve for the unrolled ansi decoding loop and for decoding of surrogates + // If there aren't enough bytes for the output, then pStop will be <= pSrc and will bypass the loop. + char* pStop = pSrc + available - 5; + if (pSrc >= pStop) + break; + + do + { + int ch = *pSrc; + pSrc++; + + if (ch > 0x7F) + { + goto LongCode; + } + *pTarget = (byte)ch; + pTarget++; + + // get pSrc aligned + if ((unchecked((int)pSrc) & 0x2) != 0) + { + ch = *pSrc; + pSrc++; + if (ch > 0x7F) + { + goto LongCode; + } + *pTarget = (byte)ch; + pTarget++; + } + + // Run 4 characters at a time! + while (pSrc < pStop) + { + ch = *(int*)pSrc; + int chc = *(int*)(pSrc + 2); + if (((ch | chc) & unchecked((int)0xFF80FF80)) != 0) + { + goto LongCodeWithMask; + } + + // Unfortunately, this is endianess sensitive +#if BIGENDIAN + *pTarget = (byte)(ch >> 16); + *(pTarget + 1) = (byte)ch; + pSrc += 4; + *(pTarget + 2) = (byte)(chc >> 16); + *(pTarget + 3) = (byte)chc; + pTarget += 4; +#else // BIGENDIAN + *pTarget = (byte)ch; + *(pTarget + 1) = (byte)(ch >> 16); + pSrc += 4; + *(pTarget + 2) = (byte)chc; + *(pTarget + 3) = (byte)(chc >> 16); + pTarget += 4; +#endif // BIGENDIAN + } + continue; + + LongCodeWithMask: +#if BIGENDIAN + // be careful about the sign extension + ch = (int)(((uint)ch) >> 16); +#else // BIGENDIAN + ch = (char)ch; +#endif // BIGENDIAN + pSrc++; + + if (ch > 0x7F) + { + goto LongCode; + } + *pTarget = (byte)ch; + pTarget++; + continue; + + LongCode: + // use separate helper variables for slow and fast loop so that the jit optimizations + // won't get confused about the variable lifetimes + int chd; + if (ch <= 0x7FF) + { + // 2 byte encoding + chd = unchecked((sbyte)0xC0) | (ch >> 6); + } + else + { + // if (!IsLowSurrogate(ch) && !IsHighSurrogate(ch)) + if (!IsInRangeInclusive(ch, JsonConstants.HighSurrogateStart, JsonConstants.LowSurrogateEnd)) + { + // 3 byte encoding + chd = unchecked((sbyte)0xE0) | (ch >> 12); + } + else + { + // 4 byte encoding - high surrogate + low surrogate + // if (!IsHighSurrogate(ch)) + if (ch > JsonConstants.HighSurrogateEnd) + { + // low without high -> bad + goto InvalidData; + } + + chd = *pSrc; + + // if (!IsLowSurrogate(chd)) { + if (!IsInRangeInclusive(chd, JsonConstants.LowSurrogateStart, JsonConstants.LowSurrogateEnd)) + { + // high not followed by low -> bad + goto InvalidData; + } + + pSrc++; + + ch = chd + (ch << 10) + + (0x10000 + - JsonConstants.LowSurrogateStart + - (JsonConstants.HighSurrogateStart << 10)); + + *pTarget = (byte)(unchecked((sbyte)0xF0) | (ch >> 18)); + // pStop - this byte is compensated by the second surrogate character + // 2 input chars require 4 output bytes. 2 have been anticipated already + // and 2 more will be accounted for by the 2 pStop-- calls below. + pTarget++; + + chd = unchecked((sbyte)0x80) | (ch >> 12) & 0x3F; + } + *pTarget = (byte)chd; + pStop--; // 3 byte sequence for 1 char, so need pStop-- and the one below too. + pTarget++; + + chd = unchecked((sbyte)0x80) | (ch >> 6) & 0x3F; + } + *pTarget = (byte)chd; + pStop--; // 2 byte sequence for 1 char so need pStop--. + + *(pTarget + 1) = (byte)(unchecked((sbyte)0x80) | ch & 0x3F); + // pStop - this byte is already included + + pTarget += 2; + } + while (pSrc < pStop); + + Debug.Assert(pTarget <= pAllocatedBufferEnd, "[UTF8Encoding.GetBytes]pTarget <= pAllocatedBufferEnd"); + } + + while (pSrc < pEnd) + { + // SLOWLOOP: does all range checks, handles all special cases, but it is slow + + // read next char. The JIT optimization seems to be getting confused when + // compiling "ch = *pSrc++;", so rather use "ch = *pSrc; pSrc++;" instead + int ch = *pSrc; + pSrc++; + + if (ch <= 0x7F) + { + if (pAllocatedBufferEnd - pTarget <= 0) + goto DestinationFull; + + *pTarget = (byte)ch; + pTarget++; + continue; + } + + int chd; + if (ch <= 0x7FF) + { + if (pAllocatedBufferEnd - pTarget <= 1) + goto DestinationFull; + + // 2 byte encoding + chd = unchecked((sbyte)0xC0) | (ch >> 6); + } + else + { + // if (!IsLowSurrogate(ch) && !IsHighSurrogate(ch)) + if (!IsInRangeInclusive(ch, JsonConstants.HighSurrogateStart, JsonConstants.LowSurrogateEnd)) + { + if (pAllocatedBufferEnd - pTarget <= 2) + goto DestinationFull; + + // 3 byte encoding + chd = unchecked((sbyte)0xE0) | (ch >> 12); + } + else + { + if (pAllocatedBufferEnd - pTarget <= 3) + goto DestinationFull; + + // 4 byte encoding - high surrogate + low surrogate + // if (!IsHighSurrogate(ch)) + if (ch > JsonConstants.HighSurrogateEnd) + { + // low without high -> bad + goto InvalidData; + } + + if (pSrc >= pEnd) + goto NeedMoreData; + + chd = *pSrc; + + // if (!IsLowSurrogate(chd)) { + if (!IsInRangeInclusive(chd, JsonConstants.LowSurrogateStart, JsonConstants.LowSurrogateEnd)) + { + // high not followed by low -> bad + goto InvalidData; + } + + pSrc++; + + ch = chd + (ch << 10) + + (0x10000 + - JsonConstants.LowSurrogateStart + - (JsonConstants.HighSurrogateStart << 10)); + + *pTarget = (byte)(unchecked((sbyte)0xF0) | (ch >> 18)); + pTarget++; + + chd = unchecked((sbyte)0x80) | (ch >> 12) & 0x3F; + } + *pTarget = (byte)chd; + pTarget++; + + chd = unchecked((sbyte)0x80) | (ch >> 6) & 0x3F; + } + + *pTarget = (byte)chd; + *(pTarget + 1) = (byte)(unchecked((sbyte)0x80) | ch & 0x3F); + + pTarget += 2; + } + + bytesConsumed = (int)((byte*)pSrc - chars); + bytesWritten = (int)(pTarget - bytes); + return OperationStatus.Done; + + InvalidData: + bytesConsumed = (int)((byte*)(pSrc - 1) - chars); + bytesWritten = (int)(pTarget - bytes); + return OperationStatus.InvalidData; + + DestinationFull: + bytesConsumed = (int)((byte*)(pSrc - 1) - chars); + bytesWritten = (int)(pTarget - bytes); + return OperationStatus.DestinationTooSmall; + + NeedMoreData: + bytesConsumed = (int)((byte*)(pSrc - 1) - chars); + bytesWritten = (int)(pTarget - bytes); + return OperationStatus.NeedMoreData; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe static int PtrDiff(char* a, char* b) + { + return (int)(((uint)((byte*)a - (byte*)b)) >> 1); + } + + // byte* flavor just for parity + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe static int PtrDiff(byte* a, byte* b) + { + return (int)(a - b); + } + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/JsonWriterHelper.cs b/src/System.Text.Json/src/System/Text/Json/Writer/JsonWriterHelper.cs new file mode 100644 index 000000000000..962731efa62b --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/JsonWriterHelper.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.Diagnostics; +using System.Runtime.CompilerServices; + +namespace System.Text.Json +{ + internal static partial class JsonWriterHelper + { + public static bool TryWriteIndentation(Span buffer, int indent, out int bytesWritten) + { + Debug.Assert(indent % JsonConstants.SpacesPerIndent == 0); + + if (buffer.Length >= indent) + { + // Based on perf tests, the break-even point where vectorized Fill is faster + // than explicitly writing the space in a loop is 8. + if (indent < 8) + { + int i = 0; + while (i < indent) + { + buffer[i++] = JsonConstants.Space; + buffer[i++] = JsonConstants.Space; + } + } + else + { + buffer.Slice(0, indent).Fill(JsonConstants.Space); + } + bytesWritten = indent; + return true; + } + else + { + int i = 0; + while (i < buffer.Length - 1) + { + buffer[i++] = JsonConstants.Space; + buffer[i++] = JsonConstants.Space; + } + bytesWritten = i; + return false; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidateProperty(ReadOnlySpan propertyName) + { + if (propertyName.Length > JsonConstants.MaxTokenSize) + ThrowHelper.ThrowArgumentException_PropertyNameTooLarge(propertyName.Length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidateValue(ReadOnlySpan value) + { + if (value.Length > JsonConstants.MaxTokenSize) + ThrowHelper.ThrowArgumentException_ValueTooLarge(value.Length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidateDouble(double value) + { + if (!double.IsFinite(value)) + ThrowHelper.ThrowArgumentException_ValueNotSupported(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidateSingle(float value) + { + if (!float.IsFinite(value)) + ThrowHelper.ThrowArgumentException_ValueNotSupported(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidateProperty(ReadOnlySpan propertyName) + { + if (propertyName.Length > JsonConstants.MaxCharacterTokenSize) + ThrowHelper.ThrowArgumentException_PropertyNameTooLarge(propertyName.Length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidateValue(ReadOnlySpan value) + { + if (value.Length > JsonConstants.MaxCharacterTokenSize) + ThrowHelper.ThrowArgumentException_ValueTooLarge(value.Length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidatePropertyAndValue(ReadOnlySpan propertyName, ReadOnlySpan value) + { + if (propertyName.Length > JsonConstants.MaxCharacterTokenSize || value.Length > JsonConstants.MaxTokenSize) + ThrowHelper.ThrowArgumentException(propertyName, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidatePropertyAndValue(ReadOnlySpan propertyName, ReadOnlySpan value) + { + if (propertyName.Length > JsonConstants.MaxTokenSize || value.Length > JsonConstants.MaxCharacterTokenSize) + ThrowHelper.ThrowArgumentException(propertyName, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidatePropertyAndValue(ReadOnlySpan propertyName, ReadOnlySpan value) + { + if (propertyName.Length > JsonConstants.MaxTokenSize || value.Length > JsonConstants.MaxTokenSize) + ThrowHelper.ThrowArgumentException(propertyName, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidatePropertyAndValue(ReadOnlySpan propertyName, ReadOnlySpan value) + { + if (propertyName.Length > JsonConstants.MaxCharacterTokenSize || value.Length > JsonConstants.MaxCharacterTokenSize) + ThrowHelper.ThrowArgumentException(propertyName, value); + } + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/JsonWriterOptions.cs b/src/System.Text.Json/src/System/Text/Json/Writer/JsonWriterOptions.cs new file mode 100644 index 000000000000..26428994f854 --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/JsonWriterOptions.cs @@ -0,0 +1,69 @@ +// 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. + +namespace System.Text.Json +{ + /// + /// Provides the ability for the user to define custom behavior when writing JSON + /// using the . By default, the JSON is written without + /// any indentation or extra white space. Also, the will + /// throw an exception if the user attempts to write structurally invalid JSON. + /// + public struct JsonWriterOptions + { + private int _optionsMask; + + /// + /// Defines whether the should pretty print the JSON which includes: + /// indenting nested JSON tokens, adding new lines, and adding white space between property names and values. + /// By default, the JSON is written without any extra white space. + /// + public bool Indented + { + get + { + return (_optionsMask & IndentBit) != 0; + } + set + { + if (value) + _optionsMask |= IndentBit; + else + _optionsMask &= ~IndentBit; + } + } + + /// + /// Defines whether the should skip structural validation and allow + /// the user to write invalid JSON, when set to true. If set to false, any attempts to write invalid JSON will result in + /// a to be thrown. + /// + /// + /// If the JSON being written is known to be correct, + /// then skipping validation (by setting it to true) could improve performance. + /// An example of invalid JSON where the writer will throw (when SkipValidation + /// is set to false) is when you write a value within a JSON object + /// without a property name. + /// + public bool SkipValidation + { + get + { + return (_optionsMask & SkipValidationBit) != 0; + } + set + { + if (value) + _optionsMask |= SkipValidationBit; + else + _optionsMask &= ~SkipValidationBit; + } + } + + internal bool IndentedOrNotSkipValidation => _optionsMask != SkipValidationBit; // Equivalent to: Indented || !SkipValidation; + + private const int IndentBit = 1; + private const int SkipValidationBit = 2; + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/JsonWriterState.cs b/src/System.Text.Json/src/System/Text/Json/Writer/JsonWriterState.cs new file mode 100644 index 000000000000..52e98d4a3f7d --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/JsonWriterState.cs @@ -0,0 +1,74 @@ +// 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. + +namespace System.Text.Json +{ + /// + /// Defines an opaque type that holds and saves all the relevant state information which must be provided + /// to the to continue writing after completing a partial write. + /// + /// + /// This type is required to support reentrancy when writing incomplete data, and to continue + /// writing in chunks. Unlike the , which is a ref struct, + /// this type can survive across async/await boundaries and hence this type is required to provide + /// support for writing more JSON text asynchronously before continuing with a new instance of the . + /// + public struct JsonWriterState + { + internal long _bytesWritten; + internal long _bytesCommitted; + internal bool _inObject; + internal bool _isNotPrimitive; + internal JsonTokenType _tokenType; + internal int _currentDepth; + internal JsonWriterOptions _writerOptions; + internal BitStack _bitStack; + + /// + /// Returns the total amount of bytes written by the so far. + /// This includes data that has been written beyond what has already been committed. + /// + public long BytesWritten => _bytesWritten; + + /// + /// Returns the total amount of bytes committed to the output by the so far. + /// This is how much the IBufferWriter has advanced. + /// + public long BytesCommitted => _bytesCommitted; + + /// + /// Constructs a new instance. + /// + /// Defines the customized behavior of the + /// By default, the writes JSON minimized (i.e. with no extra whitespace) + /// and validates that the JSON being written is structurally valid according to JSON RFC. + /// + /// An instance of this state must be passed to the ctor with the output destination. + /// Unlike the , which is a ref struct, the state can survive + /// across async/await boundaries and hence this type is required to provide support for reading + /// in more data asynchronously before continuing with a new instance of the . + /// + public JsonWriterState(JsonWriterOptions options = default) + { + _bytesWritten = default; + _bytesCommitted = default; + _inObject = default; + _isNotPrimitive = default; + _tokenType = default; + _currentDepth = default; + _writerOptions = options; + + // Only allocate if the user writes a JSON payload beyond the depth that the _allocationFreeContainer can handle. + // This way we avoid allocations in the common, default cases, and allocate lazily. + _bitStack = default; + } + + /// + /// Gets the custom behavior when writing JSON using + /// the which indicates whether to format the output + /// while writing and whether to skip structural JSON validation or not. + /// + public JsonWriterOptions Options => _writerOptions; + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/SequenceValidity.cs b/src/System.Text.Json/src/System/Text/Json/Writer/SequenceValidity.cs new file mode 100644 index 000000000000..d96c482e7358 --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/SequenceValidity.cs @@ -0,0 +1,55 @@ +// 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. + +namespace System.Buffers.Text +{ + /// + /// Represents the validity of a UTF code unit sequence. + /// + internal enum SequenceValidity + { + /// + /// The sequence is empty. + /// + Empty = 0, + + /// + /// The sequence is well-formed and unambiguously represents a proper Unicode scalar value. + /// + /// + /// [ 20 ] (U+0020 SPACE) is a well-formed UTF-8 sequence. + /// [ C3 A9 ] (U+00E9 LATIN SMALL LETTER E WITH ACUTE) is a well-formed UTF-8 sequence. + /// [ F0 9F 98 80 ] (U+1F600 GRINNING FACE) is a well-formed UTF-8 sequence. + /// [ D83D DE00 ] (U+1F600 GRINNING FACE) is a well-formed UTF-16 sequence. + /// + WellFormed = 1, + + /// + /// The sequence is not well-formed on its own, but it could appear as a prefix + /// of a longer well-formed sequence. More code units are needed to make a proper + /// determination as to whether this sequence is well-formed. Incomplete sequences + /// can only appear at the end of a string. + /// + /// + /// [ C2 ] is an incomplete UTF-8 sequence if it is followed by nothing. + /// [ F0 9F ] is an incomplete UTF-8 sequence if it is followed by nothing. + /// [ D83D ] is an incomplete UTF-16 sequence if it is followed by nothing. + /// + Incomplete = 2, + + /// + /// The sequence is never well-formed anywhere, or this sequence can never appear as a prefix + /// of a longer well-formed sequence, or the sequence was improperly terminated by the code + /// unit which appeared immediately after this sequence. + /// + /// + /// [ 80 ] is an invalid UTF-8 sequence (code unit cannot appear at start of sequence). + /// [ FE ] is an invalid UTF-8 sequence (sequence is never well-formed anywhere in UTF-8 string). + /// [ C2 ] is an invalid UTF-8 sequence if it is followed by [ 20 ] (sequence improperly terminated). + /// [ ED A0 ] is an invalid UTF-8 sequence (sequence is never well-formed anywhere in UTF-8 string). + /// [ DE00 ] is an invalid UTF-16 sequence (code unit cannot appear at start of sequence). + /// + Invalid = 3 + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.DateTime.cs b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.DateTime.cs new file mode 100644 index 000000000000..1e345c625ad4 --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.DateTime.cs @@ -0,0 +1,285 @@ +// 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.Buffers.Text; +using System.Diagnostics; + +namespace System.Text.Json +{ + public ref partial struct Utf8JsonWriter + { + /// + /// Writes the property name and value (as a JSON string) as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The value to be written as a JSON string as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the round-trippable ('O') , for example: 2017-06-12T05:30:45.7680000. + /// + public void WriteString(string propertyName, DateTime value, bool escape = true) + => WriteString(propertyName.AsSpan(), value, escape); + + /// + /// Writes the property name and value (as a JSON string) as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The value to be written as a JSON string as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the round-trippable ('O') , for example: 2017-06-12T05:30:45.7680000. + /// + public void WriteString(ReadOnlySpan propertyName, DateTime value, bool escape = true) + { + JsonWriterHelper.ValidateProperty(propertyName); + + if (escape) + { + WriteStringEscape(propertyName, value); + } + else + { + WriteStringByOptions(propertyName, value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.String; + } + + /// + /// Writes the property name and value (as a JSON string) as part of a name/value pair of a JSON object. + /// + /// The UTF-8 encoded property name of the JSON object to be written. + /// The value to be written as a JSON string as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the round-trippable ('O') , for example: 2017-06-12T05:30:45.7680000. + /// + public void WriteString(ReadOnlySpan utf8PropertyName, DateTime value, bool escape = true) + { + JsonWriterHelper.ValidateProperty(utf8PropertyName); + + if (escape) + { + WriteStringEscape(utf8PropertyName, value); + } + else + { + WriteStringByOptions(utf8PropertyName, value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.String; + } + + private void WriteStringEscape(ReadOnlySpan propertyName, DateTime value) + { + int propertyIdx = JsonWriterHelper.NeedsEscaping(propertyName); + + Debug.Assert(propertyIdx >= -1 && propertyIdx < int.MaxValue / 2); + + if (propertyIdx != -1) + { + WriteStringEscapeProperty(propertyName, value, propertyIdx); + } + else + { + WriteStringByOptions(propertyName, value); + } + } + + private void WriteStringEscape(ReadOnlySpan utf8PropertyName, DateTime value) + { + int propertyIdx = JsonWriterHelper.NeedsEscaping(utf8PropertyName); + + Debug.Assert(propertyIdx >= -1 && propertyIdx < int.MaxValue / 2); + + if (propertyIdx != -1) + { + WriteStringEscapeProperty(utf8PropertyName, value, propertyIdx); + } + else + { + WriteStringByOptions(utf8PropertyName, value); + } + } + + private void WriteStringEscapeProperty(ReadOnlySpan propertyName, DateTime value, int firstEscapeIndexProp) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= propertyName.Length); + Debug.Assert(firstEscapeIndexProp >= 0 && firstEscapeIndexProp < propertyName.Length); + + char[] propertyArray = null; + + int length = JsonWriterHelper.GetMaxEscapedLength(propertyName.Length, firstEscapeIndexProp); + Span escapedPropertyName; + if (length > StackallocThreshold) + { + propertyArray = ArrayPool.Shared.Rent(length); + escapedPropertyName = propertyArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + char* ptr = stackalloc char[length]; + escapedPropertyName = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(propertyName, escapedPropertyName, firstEscapeIndexProp, out int written); + + WriteStringByOptions(escapedPropertyName.Slice(0, written), value); + + if (propertyArray != null) + { + ArrayPool.Shared.Return(propertyArray); + } + } + + private void WriteStringEscapeProperty(ReadOnlySpan utf8PropertyName, DateTime value, int firstEscapeIndexProp) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= utf8PropertyName.Length); + Debug.Assert(firstEscapeIndexProp >= 0 && firstEscapeIndexProp < utf8PropertyName.Length); + + byte[] propertyArray = null; + + int length = JsonWriterHelper.GetMaxEscapedLength(utf8PropertyName.Length, firstEscapeIndexProp); + Span escapedPropertyName; + if (length > StackallocThreshold) + { + propertyArray = ArrayPool.Shared.Rent(length); + escapedPropertyName = propertyArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + byte* ptr = stackalloc byte[length]; + escapedPropertyName = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written); + + WriteStringByOptions(escapedPropertyName.Slice(0, written), value); + + if (propertyArray != null) + { + ArrayPool.Shared.Return(propertyArray); + } + } + + private void WriteStringByOptions(ReadOnlySpan propertyName, DateTime value) + { + ValidateWritingProperty(); + if (_writerOptions.Indented) + { + WriteStringIndented(propertyName, value); + } + else + { + WriteStringMinimized(propertyName, value); + } + } + + private void WriteStringByOptions(ReadOnlySpan utf8PropertyName, DateTime value) + { + ValidateWritingProperty(); + if (_writerOptions.Indented) + { + WriteStringIndented(utf8PropertyName, value); + } + else + { + WriteStringMinimized(utf8PropertyName, value); + } + } + + private void WriteStringMinimized(ReadOnlySpan escapedPropertyName, DateTime value) + { + int idx = WritePropertyNameMinimized(escapedPropertyName); + + WriteStringValue(value, ref idx); + + Advance(idx); + } + + private void WriteStringMinimized(ReadOnlySpan escapedPropertyName, DateTime value) + { + int idx = WritePropertyNameMinimized(escapedPropertyName); + + WriteStringValue(value, ref idx); + + Advance(idx); + } + + private void WriteStringIndented(ReadOnlySpan escapedPropertyName, DateTime value) + { + int idx = WritePropertyNameIndented(escapedPropertyName); + + WriteStringValue(value, ref idx); + + Advance(idx); + } + + private void WriteStringIndented(ReadOnlySpan escapedPropertyName, DateTime value) + { + int idx = WritePropertyNameIndented(escapedPropertyName); + + WriteStringValue(value, ref idx); + + Advance(idx); + } + + private void WriteStringValue(DateTime value, ref int idx) + { + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Quote; + + FormatLoop(value, ref idx); + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Quote; + } + + private static readonly StandardFormat s_dateTimeStandardFormat = new StandardFormat('O'); + + private void FormatLoop(DateTime value, ref int idx) + { + if (!Utf8Formatter.TryFormat(value, _buffer.Slice(idx), out int bytesWritten, s_dateTimeStandardFormat)) + { + AdvanceAndGrow(ref idx, JsonConstants.MaximumFormatDateTimeLength); + bool result = Utf8Formatter.TryFormat(value, _buffer, out bytesWritten, s_dateTimeStandardFormat); + Debug.Assert(result); + } + idx += bytesWritten; + } + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.DateTimeOffset.cs b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.DateTimeOffset.cs new file mode 100644 index 000000000000..16d61c6de8d7 --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.DateTimeOffset.cs @@ -0,0 +1,283 @@ +// 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.Buffers.Text; +using System.Diagnostics; + +namespace System.Text.Json +{ + public ref partial struct Utf8JsonWriter + { + /// + /// Writes the property name and value (as a JSON string) as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The value to be written as a JSON string as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the round-trippable ('O') , for example: 2017-06-12T05:30:45.7680000-07:00. + /// + public void WriteString(string propertyName, DateTimeOffset value, bool escape = true) + => WriteString(propertyName.AsSpan(), value, escape); + + /// + /// Writes the property name and value (as a JSON string) as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The value to be written as a JSON string as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the round-trippable ('O') , for example: 2017-06-12T05:30:45.7680000-07:00. + /// + public void WriteString(ReadOnlySpan propertyName, DateTimeOffset value, bool escape = true) + { + JsonWriterHelper.ValidateProperty(propertyName); + + if (escape) + { + WriteStringEscape(propertyName, value); + } + else + { + WriteStringByOptions(propertyName, value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.String; + } + + /// + /// Writes the property name and value (as a JSON string) as part of a name/value pair of a JSON object. + /// + /// The UTF-8 encoded property name of the JSON object to be written. + /// The value to be written as a JSON string as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the round-trippable ('O') , for example: 2017-06-12T05:30:45.7680000-07:00. + /// + public void WriteString(ReadOnlySpan utf8PropertyName, DateTimeOffset value, bool escape = true) + { + JsonWriterHelper.ValidateProperty(utf8PropertyName); + + if (escape) + { + WriteStringEscape(utf8PropertyName, value); + } + else + { + WriteStringByOptions(utf8PropertyName, value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.String; + } + + private void WriteStringEscape(ReadOnlySpan propertyName, DateTimeOffset value) + { + int propertyIdx = JsonWriterHelper.NeedsEscaping(propertyName); + + Debug.Assert(propertyIdx >= -1 && propertyIdx < int.MaxValue / 2); + + if (propertyIdx != -1) + { + WriteStringEscapeProperty(propertyName, value, propertyIdx); + } + else + { + WriteStringByOptions(propertyName, value); + } + } + + private void WriteStringEscape(ReadOnlySpan utf8PropertyName, DateTimeOffset value) + { + int propertyIdx = JsonWriterHelper.NeedsEscaping(utf8PropertyName); + + Debug.Assert(propertyIdx >= -1 && propertyIdx < int.MaxValue / 2); + + if (propertyIdx != -1) + { + WriteStringEscapeProperty(utf8PropertyName, value, propertyIdx); + } + else + { + WriteStringByOptions(utf8PropertyName, value); + } + } + + private void WriteStringEscapeProperty(ReadOnlySpan propertyName, DateTimeOffset value, int firstEscapeIndexProp) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= propertyName.Length); + Debug.Assert(firstEscapeIndexProp >= 0 && firstEscapeIndexProp < propertyName.Length); + + char[] propertyArray = null; + + int length = JsonWriterHelper.GetMaxEscapedLength(propertyName.Length, firstEscapeIndexProp); + Span escapedPropertyName; + if (length > StackallocThreshold) + { + propertyArray = ArrayPool.Shared.Rent(length); + escapedPropertyName = propertyArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + char* ptr = stackalloc char[length]; + escapedPropertyName = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(propertyName, escapedPropertyName, firstEscapeIndexProp, out int written); + + WriteStringByOptions(escapedPropertyName.Slice(0, written), value); + + if (propertyArray != null) + { + ArrayPool.Shared.Return(propertyArray); + } + } + + private void WriteStringEscapeProperty(ReadOnlySpan utf8PropertyName, DateTimeOffset value, int firstEscapeIndexProp) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= utf8PropertyName.Length); + Debug.Assert(firstEscapeIndexProp >= 0 && firstEscapeIndexProp < utf8PropertyName.Length); + + byte[] propertyArray = null; + + int length = JsonWriterHelper.GetMaxEscapedLength(utf8PropertyName.Length, firstEscapeIndexProp); + Span escapedPropertyName; + if (length > StackallocThreshold) + { + propertyArray = ArrayPool.Shared.Rent(length); + escapedPropertyName = propertyArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + byte* ptr = stackalloc byte[length]; + escapedPropertyName = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written); + + WriteStringByOptions(escapedPropertyName.Slice(0, written), value); + + if (propertyArray != null) + { + ArrayPool.Shared.Return(propertyArray); + } + } + + private void WriteStringByOptions(ReadOnlySpan propertyName, DateTimeOffset value) + { + ValidateWritingProperty(); + if (_writerOptions.Indented) + { + WriteStringIndented(propertyName, value); + } + else + { + WriteStringMinimized(propertyName, value); + } + } + + private void WriteStringByOptions(ReadOnlySpan utf8PropertyName, DateTimeOffset value) + { + ValidateWritingProperty(); + if (_writerOptions.Indented) + { + WriteStringIndented(utf8PropertyName, value); + } + else + { + WriteStringMinimized(utf8PropertyName, value); + } + } + + private void WriteStringMinimized(ReadOnlySpan escapedPropertyName, DateTimeOffset value) + { + int idx = WritePropertyNameMinimized(escapedPropertyName); + + WriteStringValue(value, ref idx); + + Advance(idx); + } + + private void WriteStringMinimized(ReadOnlySpan escapedPropertyName, DateTimeOffset value) + { + int idx = WritePropertyNameMinimized(escapedPropertyName); + + WriteStringValue(value, ref idx); + + Advance(idx); + } + + private void WriteStringIndented(ReadOnlySpan escapedPropertyName, DateTimeOffset value) + { + int idx = WritePropertyNameIndented(escapedPropertyName); + + WriteStringValue(value, ref idx); + + Advance(idx); + } + + private void WriteStringIndented(ReadOnlySpan escapedPropertyName, DateTimeOffset value) + { + int idx = WritePropertyNameIndented(escapedPropertyName); + + WriteStringValue(value, ref idx); + + Advance(idx); + } + + private void WriteStringValue(DateTimeOffset value, ref int idx) + { + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Quote; + + FormatLoop(value, ref idx); + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Quote; + } + + private void FormatLoop(DateTimeOffset value, ref int idx) + { + if (!Utf8Formatter.TryFormat(value, _buffer.Slice(idx), out int bytesWritten, s_dateTimeStandardFormat)) + { + AdvanceAndGrow(ref idx, JsonConstants.MaximumFormatDateTimeOffsetLength); + bool result = Utf8Formatter.TryFormat(value, _buffer, out bytesWritten, s_dateTimeStandardFormat); + Debug.Assert(result); + } + idx += bytesWritten; + } + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Decimal.cs b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Decimal.cs new file mode 100644 index 000000000000..f03e84d07d11 --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Decimal.cs @@ -0,0 +1,266 @@ +// 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.Buffers.Text; +using System.Diagnostics; + +namespace System.Text.Json +{ + public ref partial struct Utf8JsonWriter + { + /// + /// Writes the property name and value (as a JSON number) as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The value to be written as a JSON number as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'). + /// + public void WriteNumber(string propertyName, decimal value, bool escape = true) + => WriteNumber(propertyName.AsSpan(), value, escape); + + /// + /// Writes the property name and value (as a JSON number) as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The value to be written as a JSON number as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'). + /// + public void WriteNumber(ReadOnlySpan propertyName, decimal value, bool escape = true) + { + JsonWriterHelper.ValidateProperty(propertyName); + + if (escape) + { + WriteNumberEscape(propertyName, value); + } + else + { + WriteNumberByOptions(propertyName, value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.Number; + } + + /// + /// Writes the property name and value (as a JSON number) as part of a name/value pair of a JSON object. + /// + /// The UTF-8 encoded property name of the JSON object to be written. + /// The value to be written as a JSON number as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'). + /// + public void WriteNumber(ReadOnlySpan utf8PropertyName, decimal value, bool escape = true) + { + JsonWriterHelper.ValidateProperty(utf8PropertyName); + + if (escape) + { + WriteNumberEscape(utf8PropertyName, value); + } + else + { + WriteNumberByOptions(utf8PropertyName, value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.Number; + } + + private void WriteNumberEscape(ReadOnlySpan propertyName, decimal value) + { + int propertyIdx = JsonWriterHelper.NeedsEscaping(propertyName); + + Debug.Assert(propertyIdx >= -1 && propertyIdx < int.MaxValue / 2); + + if (propertyIdx != -1) + { + WriteNumberEscapeProperty(propertyName, value, propertyIdx); + } + else + { + WriteNumberByOptions(propertyName, value); + } + } + + private void WriteNumberEscape(ReadOnlySpan utf8PropertyName, decimal value) + { + int propertyIdx = JsonWriterHelper.NeedsEscaping(utf8PropertyName); + + Debug.Assert(propertyIdx >= -1 && propertyIdx < int.MaxValue / 2); + + if (propertyIdx != -1) + { + WriteNumberEscapeProperty(utf8PropertyName, value, propertyIdx); + } + else + { + WriteNumberByOptions(utf8PropertyName, value); + } + } + + private void WriteNumberEscapeProperty(ReadOnlySpan propertyName, decimal value, int firstEscapeIndexProp) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= propertyName.Length); + Debug.Assert(firstEscapeIndexProp >= 0 && firstEscapeIndexProp < propertyName.Length); + + char[] propertyArray = null; + + int length = JsonWriterHelper.GetMaxEscapedLength(propertyName.Length, firstEscapeIndexProp); + Span escapedPropertyName; + if (length > StackallocThreshold) + { + propertyArray = ArrayPool.Shared.Rent(length); + escapedPropertyName = propertyArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + char* ptr = stackalloc char[length]; + escapedPropertyName = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(propertyName, escapedPropertyName, firstEscapeIndexProp, out int written); + + WriteNumberByOptions(escapedPropertyName.Slice(0, written), value); + + if (propertyArray != null) + { + ArrayPool.Shared.Return(propertyArray); + } + } + + private void WriteNumberEscapeProperty(ReadOnlySpan utf8PropertyName, decimal value, int firstEscapeIndexProp) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= utf8PropertyName.Length); + Debug.Assert(firstEscapeIndexProp >= 0 && firstEscapeIndexProp < utf8PropertyName.Length); + + byte[] propertyArray = null; + + int length = JsonWriterHelper.GetMaxEscapedLength(utf8PropertyName.Length, firstEscapeIndexProp); + Span escapedPropertyName; + if (length > StackallocThreshold) + { + propertyArray = ArrayPool.Shared.Rent(length); + escapedPropertyName = propertyArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + byte* ptr = stackalloc byte[length]; + escapedPropertyName = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written); + + WriteNumberByOptions(escapedPropertyName.Slice(0, written), value); + + if (propertyArray != null) + { + ArrayPool.Shared.Return(propertyArray); + } + } + + private void WriteNumberByOptions(ReadOnlySpan propertyName, decimal value) + { + ValidateWritingProperty(); + if (_writerOptions.Indented) + { + WriteNumberIndented(propertyName, value); + } + else + { + WriteNumberMinimized(propertyName, value); + } + } + + private void WriteNumberByOptions(ReadOnlySpan utf8PropertyName, decimal value) + { + ValidateWritingProperty(); + if (_writerOptions.Indented) + { + WriteNumberIndented(utf8PropertyName, value); + } + else + { + WriteNumberMinimized(utf8PropertyName, value); + } + } + + private void WriteNumberMinimized(ReadOnlySpan escapedPropertyName, decimal value) + { + int idx = WritePropertyNameMinimized(escapedPropertyName); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + + private void WriteNumberMinimized(ReadOnlySpan escapedPropertyName, decimal value) + { + int idx = WritePropertyNameMinimized(escapedPropertyName); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + + private void WriteNumberIndented(ReadOnlySpan escapedPropertyName, decimal value) + { + int idx = WritePropertyNameIndented(escapedPropertyName); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + + private void WriteNumberIndented(ReadOnlySpan escapedPropertyName, decimal value) + { + int idx = WritePropertyNameIndented(escapedPropertyName); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + + private void WriteNumberValueFormatLoop(decimal value, ref int idx) + { + if (!Utf8Formatter.TryFormat(value, _buffer.Slice(idx), out int bytesWritten)) + { + AdvanceAndGrow(ref idx, JsonConstants.MaximumFormatDecimalLength); + bool result = Utf8Formatter.TryFormat(value, _buffer, out bytesWritten); + Debug.Assert(result); + } + idx += bytesWritten; + } + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Double.cs b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Double.cs new file mode 100644 index 000000000000..ea19ce3a838c --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Double.cs @@ -0,0 +1,268 @@ +// 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.Buffers.Text; +using System.Diagnostics; + +namespace System.Text.Json +{ + public ref partial struct Utf8JsonWriter + { + /// + /// Writes the property name and value (as a JSON number) as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The value to be written as a JSON number as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'). + /// + public void WriteNumber(string propertyName, double value, bool escape = true) + => WriteNumber(propertyName.AsSpan(), value, escape); + + /// + /// Writes the property name and value (as a JSON number) as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The value to be written as a JSON number as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'). + /// + public void WriteNumber(ReadOnlySpan propertyName, double value, bool escape = true) + { + JsonWriterHelper.ValidateProperty(propertyName); + JsonWriterHelper.ValidateDouble(value); + + if (escape) + { + WriteNumberEscape(propertyName, value); + } + else + { + WriteNumberByOptions(propertyName, value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.Number; + } + + /// + /// Writes the property name and value (as a JSON number) as part of a name/value pair of a JSON object. + /// + /// The UTF-8 encoded property name of the JSON object to be written. + /// The value to be written as a JSON number as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'). + /// + public void WriteNumber(ReadOnlySpan utf8PropertyName, double value, bool escape = true) + { + JsonWriterHelper.ValidateProperty(utf8PropertyName); + JsonWriterHelper.ValidateDouble(value); + + if (escape) + { + WriteNumberEscape(utf8PropertyName, value); + } + else + { + WriteNumberByOptions(utf8PropertyName, value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.Number; + } + + private void WriteNumberEscape(ReadOnlySpan propertyName, double value) + { + int propertyIdx = JsonWriterHelper.NeedsEscaping(propertyName); + + Debug.Assert(propertyIdx >= -1 && propertyIdx < int.MaxValue / 2); + + if (propertyIdx != -1) + { + WriteNumberEscapeProperty(propertyName, value, propertyIdx); + } + else + { + WriteNumberByOptions(propertyName, value); + } + } + + private void WriteNumberEscape(ReadOnlySpan utf8PropertyName, double value) + { + int propertyIdx = JsonWriterHelper.NeedsEscaping(utf8PropertyName); + + Debug.Assert(propertyIdx >= -1 && propertyIdx < int.MaxValue / 2); + + if (propertyIdx != -1) + { + WriteNumberEscapeProperty(utf8PropertyName, value, propertyIdx); + } + else + { + WriteNumberByOptions(utf8PropertyName, value); + } + } + + private void WriteNumberEscapeProperty(ReadOnlySpan propertyName, double value, int firstEscapeIndexProp) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= propertyName.Length); + Debug.Assert(firstEscapeIndexProp >= 0 && firstEscapeIndexProp < propertyName.Length); + + char[] propertyArray = null; + + int length = JsonWriterHelper.GetMaxEscapedLength(propertyName.Length, firstEscapeIndexProp); + Span escapedPropertyName; + if (length > StackallocThreshold) + { + propertyArray = ArrayPool.Shared.Rent(length); + escapedPropertyName = propertyArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + char* ptr = stackalloc char[length]; + escapedPropertyName = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(propertyName, escapedPropertyName, firstEscapeIndexProp, out int written); + + WriteNumberByOptions(escapedPropertyName.Slice(0, written), value); + + if (propertyArray != null) + { + ArrayPool.Shared.Return(propertyArray); + } + } + + private void WriteNumberEscapeProperty(ReadOnlySpan utf8PropertyName, double value, int firstEscapeIndexProp) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= utf8PropertyName.Length); + Debug.Assert(firstEscapeIndexProp >= 0 && firstEscapeIndexProp < utf8PropertyName.Length); + + byte[] propertyArray = null; + + int length = JsonWriterHelper.GetMaxEscapedLength(utf8PropertyName.Length, firstEscapeIndexProp); + Span escapedPropertyName; + if (length > StackallocThreshold) + { + propertyArray = ArrayPool.Shared.Rent(length); + escapedPropertyName = propertyArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + byte* ptr = stackalloc byte[length]; + escapedPropertyName = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written); + + WriteNumberByOptions(escapedPropertyName.Slice(0, written), value); + + if (propertyArray != null) + { + ArrayPool.Shared.Return(propertyArray); + } + } + + private void WriteNumberByOptions(ReadOnlySpan propertyName, double value) + { + ValidateWritingProperty(); + if (_writerOptions.Indented) + { + WriteNumberIndented(propertyName, value); + } + else + { + WriteNumberMinimized(propertyName, value); + } + } + + private void WriteNumberByOptions(ReadOnlySpan utf8PropertyName, double value) + { + ValidateWritingProperty(); + if (_writerOptions.Indented) + { + WriteNumberIndented(utf8PropertyName, value); + } + else + { + WriteNumberMinimized(utf8PropertyName, value); + } + } + + private void WriteNumberMinimized(ReadOnlySpan escapedPropertyName, double value) + { + int idx = WritePropertyNameMinimized(escapedPropertyName); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + + private void WriteNumberMinimized(ReadOnlySpan escapedPropertyName, double value) + { + int idx = WritePropertyNameMinimized(escapedPropertyName); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + + private void WriteNumberIndented(ReadOnlySpan escapedPropertyName, double value) + { + int idx = WritePropertyNameIndented(escapedPropertyName); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + + private void WriteNumberIndented(ReadOnlySpan escapedPropertyName, double value) + { + int idx = WritePropertyNameIndented(escapedPropertyName); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + + private void WriteNumberValueFormatLoop(double value, ref int idx) + { + if (!Utf8Formatter.TryFormat(value, _buffer.Slice(idx), out int bytesWritten)) + { + AdvanceAndGrow(ref idx, JsonConstants.MaximumFormatDoubleLength); + bool result = Utf8Formatter.TryFormat(value, _buffer, out bytesWritten); + Debug.Assert(result); + } + idx += bytesWritten; + } + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Float.cs b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Float.cs new file mode 100644 index 000000000000..5853265c4a68 --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Float.cs @@ -0,0 +1,268 @@ +// 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.Buffers.Text; +using System.Diagnostics; + +namespace System.Text.Json +{ + public ref partial struct Utf8JsonWriter + { + /// + /// Writes the property name and value (as a JSON number) as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The value to be written as a JSON number as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'). + /// + public void WriteNumber(string propertyName, float value, bool escape = true) + => WriteNumber(propertyName.AsSpan(), value, escape); + + /// + /// Writes the property name and value (as a JSON number) as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The value to be written as a JSON number as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'). + /// + public void WriteNumber(ReadOnlySpan propertyName, float value, bool escape = true) + { + JsonWriterHelper.ValidateProperty(propertyName); + JsonWriterHelper.ValidateSingle(value); + + if (escape) + { + WriteNumberEscape(propertyName, value); + } + else + { + WriteNumberByOptions(propertyName, value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.Number; + } + + /// + /// Writes the property name and value (as a JSON number) as part of a name/value pair of a JSON object. + /// + /// The UTF-8 encoded property name of the JSON object to be written. + /// The value to be written as a JSON number as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'). + /// + public void WriteNumber(ReadOnlySpan utf8PropertyName, float value, bool escape = true) + { + JsonWriterHelper.ValidateProperty(utf8PropertyName); + JsonWriterHelper.ValidateSingle(value); + + if (escape) + { + WriteNumberEscape(utf8PropertyName, value); + } + else + { + WriteNumberByOptions(utf8PropertyName, value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.Number; + } + + private void WriteNumberEscape(ReadOnlySpan propertyName, float value) + { + int propertyIdx = JsonWriterHelper.NeedsEscaping(propertyName); + + Debug.Assert(propertyIdx >= -1 && propertyIdx < int.MaxValue / 2); + + if (propertyIdx != -1) + { + WriteNumberEscapeProperty(propertyName, value, propertyIdx); + } + else + { + WriteNumberByOptions(propertyName, value); + } + } + + private void WriteNumberEscape(ReadOnlySpan utf8PropertyName, float value) + { + int propertyIdx = JsonWriterHelper.NeedsEscaping(utf8PropertyName); + + Debug.Assert(propertyIdx >= -1 && propertyIdx < int.MaxValue / 2); + + if (propertyIdx != -1) + { + WriteNumberEscapeProperty(utf8PropertyName, value, propertyIdx); + } + else + { + WriteNumberByOptions(utf8PropertyName, value); + } + } + + private void WriteNumberEscapeProperty(ReadOnlySpan propertyName, float value, int firstEscapeIndexProp) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= propertyName.Length); + Debug.Assert(firstEscapeIndexProp >= 0 && firstEscapeIndexProp < propertyName.Length); + + char[] propertyArray = null; + + int length = JsonWriterHelper.GetMaxEscapedLength(propertyName.Length, firstEscapeIndexProp); + Span escapedPropertyName; + if (length > StackallocThreshold) + { + propertyArray = ArrayPool.Shared.Rent(length); + escapedPropertyName = propertyArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + char* ptr = stackalloc char[length]; + escapedPropertyName = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(propertyName, escapedPropertyName, firstEscapeIndexProp, out int written); + + WriteNumberByOptions(escapedPropertyName.Slice(0, written), value); + + if (propertyArray != null) + { + ArrayPool.Shared.Return(propertyArray); + } + } + + private void WriteNumberEscapeProperty(ReadOnlySpan utf8PropertyName, float value, int firstEscapeIndexProp) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= utf8PropertyName.Length); + Debug.Assert(firstEscapeIndexProp >= 0 && firstEscapeIndexProp < utf8PropertyName.Length); + + byte[] propertyArray = null; + + int length = JsonWriterHelper.GetMaxEscapedLength(utf8PropertyName.Length, firstEscapeIndexProp); + Span escapedPropertyName; + if (length > StackallocThreshold) + { + propertyArray = ArrayPool.Shared.Rent(length); + escapedPropertyName = propertyArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + byte* ptr = stackalloc byte[length]; + escapedPropertyName = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written); + + WriteNumberByOptions(escapedPropertyName.Slice(0, written), value); + + if (propertyArray != null) + { + ArrayPool.Shared.Return(propertyArray); + } + } + + private void WriteNumberByOptions(ReadOnlySpan propertyName, float value) + { + ValidateWritingProperty(); + if (_writerOptions.Indented) + { + WriteNumberIndented(propertyName, value); + } + else + { + WriteNumberMinimized(propertyName, value); + } + } + + private void WriteNumberByOptions(ReadOnlySpan propertyName, float value) + { + ValidateWritingProperty(); + if (_writerOptions.Indented) + { + WriteNumberIndented(propertyName, value); + } + else + { + WriteNumberMinimized(propertyName, value); + } + } + + private void WriteNumberMinimized(ReadOnlySpan escapedPropertyName, float value) + { + int idx = WritePropertyNameMinimized(escapedPropertyName); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + + private void WriteNumberMinimized(ReadOnlySpan escapedPropertyName, float value) + { + int idx = WritePropertyNameMinimized(escapedPropertyName); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + + private void WriteNumberIndented(ReadOnlySpan escapedPropertyName, float value) + { + int idx = WritePropertyNameIndented(escapedPropertyName); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + + private void WriteNumberIndented(ReadOnlySpan escapedPropertyName, float value) + { + int idx = WritePropertyNameIndented(escapedPropertyName); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + + private void WriteNumberValueFormatLoop(float value, ref int idx) + { + if (!Utf8Formatter.TryFormat(value, _buffer.Slice(idx), out int bytesWritten)) + { + AdvanceAndGrow(ref idx, JsonConstants.MaximumFormatSingleLength); + bool result = Utf8Formatter.TryFormat(value, _buffer, out bytesWritten); + Debug.Assert(result); + } + idx += bytesWritten; + } + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Guid.cs b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Guid.cs new file mode 100644 index 000000000000..c201b6afa1b3 --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Guid.cs @@ -0,0 +1,283 @@ +// 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.Buffers.Text; +using System.Diagnostics; + +namespace System.Text.Json +{ + public ref partial struct Utf8JsonWriter + { + /// + /// Writes the property name and value (as a JSON string) as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The value to be written as a JSON string as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'D'), as the form: nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn. + /// + public void WriteString(string propertyName, Guid value, bool escape = true) + => WriteString(propertyName.AsSpan(), value, escape); + + /// + /// Writes the property name and value (as a JSON string) as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The value to be written as a JSON string as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'D'), as the form: nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn. + /// + public void WriteString(ReadOnlySpan propertyName, Guid value, bool escape = true) + { + JsonWriterHelper.ValidateProperty(propertyName); + + if (escape) + { + WriteStringEscape(propertyName, value); + } + else + { + WriteStringByOptions(propertyName, value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.String; + } + + /// + /// Writes the property name and value (as a JSON string) as part of a name/value pair of a JSON object. + /// + /// The UTF-8 encoded property name of the JSON object to be written. + /// The value to be written as a JSON string as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'D'), as the form: nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn. + /// + public void WriteString(ReadOnlySpan utf8PropertyName, Guid value, bool escape = true) + { + JsonWriterHelper.ValidateProperty(utf8PropertyName); + + if (escape) + { + WriteStringEscape(utf8PropertyName, value); + } + else + { + WriteStringByOptions(utf8PropertyName, value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.String; + } + + private void WriteStringEscape(ReadOnlySpan propertyName, Guid value) + { + int propertyIdx = JsonWriterHelper.NeedsEscaping(propertyName); + + Debug.Assert(propertyIdx >= -1 && propertyIdx < int.MaxValue / 2); + + if (propertyIdx != -1) + { + WriteStringEscapeProperty(propertyName, value, propertyIdx); + } + else + { + WriteStringByOptions(propertyName, value); + } + } + + private void WriteStringEscape(ReadOnlySpan utf8PropertyName, Guid value) + { + int propertyIdx = JsonWriterHelper.NeedsEscaping(utf8PropertyName); + + Debug.Assert(propertyIdx >= -1 && propertyIdx < int.MaxValue / 2); + + if (propertyIdx != -1) + { + WriteStringEscapeProperty(utf8PropertyName, value, propertyIdx); + } + else + { + WriteStringByOptions(utf8PropertyName, value); + } + } + + private void WriteStringEscapeProperty(ReadOnlySpan propertyName, Guid value, int firstEscapeIndexProp) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= propertyName.Length); + Debug.Assert(firstEscapeIndexProp >= 0 && firstEscapeIndexProp < propertyName.Length); + + char[] propertyArray = null; + + int length = JsonWriterHelper.GetMaxEscapedLength(propertyName.Length, firstEscapeIndexProp); + Span escapedPropertyName; + if (length > StackallocThreshold) + { + propertyArray = ArrayPool.Shared.Rent(length); + escapedPropertyName = propertyArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + char* ptr = stackalloc char[length]; + escapedPropertyName = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(propertyName, escapedPropertyName, firstEscapeIndexProp, out int written); + + WriteStringByOptions(escapedPropertyName.Slice(0, written), value); + + if (propertyArray != null) + { + ArrayPool.Shared.Return(propertyArray); + } + } + + private void WriteStringEscapeProperty(ReadOnlySpan utf8PropertyName, Guid value, int firstEscapeIndexProp) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= utf8PropertyName.Length); + Debug.Assert(firstEscapeIndexProp >= 0 && firstEscapeIndexProp < utf8PropertyName.Length); + + byte[] propertyArray = null; + + int length = JsonWriterHelper.GetMaxEscapedLength(utf8PropertyName.Length, firstEscapeIndexProp); + Span escapedPropertyName; + if (length > StackallocThreshold) + { + propertyArray = ArrayPool.Shared.Rent(length); + escapedPropertyName = propertyArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + byte* ptr = stackalloc byte[length]; + escapedPropertyName = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written); + + WriteStringByOptions(escapedPropertyName.Slice(0, written), value); + + if (propertyArray != null) + { + ArrayPool.Shared.Return(propertyArray); + } + } + + private void WriteStringByOptions(ReadOnlySpan propertyName, Guid value) + { + ValidateWritingProperty(); + if (_writerOptions.Indented) + { + WriteStringIndented(propertyName, value); + } + else + { + WriteStringMinimized(propertyName, value); + } + } + + private void WriteStringByOptions(ReadOnlySpan utf8PropertyName, Guid value) + { + ValidateWritingProperty(); + if (_writerOptions.Indented) + { + WriteStringIndented(utf8PropertyName, value); + } + else + { + WriteStringMinimized(utf8PropertyName, value); + } + } + + private void WriteStringMinimized(ReadOnlySpan escapedPropertyName, Guid value) + { + int idx = WritePropertyNameMinimized(escapedPropertyName); + + WriteStringValue(value, ref idx); + + Advance(idx); + } + + private void WriteStringMinimized(ReadOnlySpan escapedPropertyName, Guid value) + { + int idx = WritePropertyNameMinimized(escapedPropertyName); + + WriteStringValue(value, ref idx); + + Advance(idx); + } + + private void WriteStringIndented(ReadOnlySpan escapedPropertyName, Guid value) + { + int idx = WritePropertyNameIndented(escapedPropertyName); + + WriteStringValue(value, ref idx); + + Advance(idx); + } + + private void WriteStringIndented(ReadOnlySpan escapedPropertyName, Guid value) + { + int idx = WritePropertyNameIndented(escapedPropertyName); + + WriteStringValue(value, ref idx); + + Advance(idx); + } + + private void WriteStringValue(Guid value, ref int idx) + { + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Quote; + + FormatLoop(value, ref idx); + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Quote; + } + + private void FormatLoop(Guid value, ref int idx) + { + if (!Utf8Formatter.TryFormat(value, _buffer.Slice(idx), out int bytesWritten)) + { + AdvanceAndGrow(ref idx, JsonConstants.MaximumFormatGuidLength); + bool result = Utf8Formatter.TryFormat(value, _buffer, out bytesWritten); + Debug.Assert(result); + } + idx += bytesWritten; + } + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Helpers.cs b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Helpers.cs new file mode 100644 index 000000000000..a7191a57e6ec --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Helpers.cs @@ -0,0 +1,263 @@ +// 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.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace System.Text.Json +{ + public ref partial struct Utf8JsonWriter + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ValidatePropertyNameAndDepth(ReadOnlySpan propertyName) + { + if (propertyName.Length > JsonConstants.MaxCharacterTokenSize || CurrentDepth >= JsonConstants.MaxWriterDepth) + ThrowHelper.ThrowInvalidOperationOrArgumentException(propertyName, _currentDepth); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ValidatePropertyNameAndDepth(ReadOnlySpan utf8PropertyName) + { + if (utf8PropertyName.Length > JsonConstants.MaxTokenSize || CurrentDepth >= JsonConstants.MaxWriterDepth) + ThrowHelper.ThrowInvalidOperationOrArgumentException(utf8PropertyName, _currentDepth); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ValidateWritingProperty() + { + if (!_writerOptions.SkipValidation) + { + if (!_inObject) + { + Debug.Assert(_tokenType != JsonTokenType.StartObject); + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.CannotWritePropertyWithinArray, tokenType: _tokenType); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ValidateWritingProperty(byte token) + { + if (!_writerOptions.SkipValidation) + { + if (!_inObject) + { + Debug.Assert(_tokenType != JsonTokenType.StartObject); + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.CannotWritePropertyWithinArray, tokenType: _tokenType); + } + UpdateBitStackOnStart(token); + } + } + + private int WritePropertyNameMinimized(ReadOnlySpan escapedPropertyName) + { + int idx = 0; + if (_currentDepth < 0) + { + if (_buffer.Length <= idx) + { + GrowAndEnsure(); + } + _buffer[idx++] = JsonConstants.ListSeparator; + } + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Quote; + + CopyLoop(escapedPropertyName, ref idx); + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Quote; + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.KeyValueSeperator; + + return idx; + } + + private int WritePropertyNameIndented(ReadOnlySpan escapedPropertyName) + { + int idx = 0; + if (_currentDepth < 0) + { + if (_buffer.Length <= idx) + { + GrowAndEnsure(); + } + _buffer[idx++] = JsonConstants.ListSeparator; + } + + if (_tokenType != JsonTokenType.None) + WriteNewLine(ref idx); + + int indent = Indentation; + while (true) + { + bool result = JsonWriterHelper.TryWriteIndentation(_buffer.Slice(idx), indent, out int bytesWritten); + idx += bytesWritten; + if (result) + { + break; + } + indent -= bytesWritten; + AdvanceAndGrow(ref idx); + } + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Quote; + + CopyLoop(escapedPropertyName, ref idx); + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Quote; + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.KeyValueSeperator; + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Space; + + return idx; + } + + private int WritePropertyNameMinimized(ReadOnlySpan escapedPropertyName) + { + int idx = 0; + if (_currentDepth < 0) + { + if (_buffer.Length <= idx) + { + GrowAndEnsure(); + } + _buffer[idx++] = JsonConstants.ListSeparator; + } + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Quote; + + ReadOnlySpan byteSpan = MemoryMarshal.AsBytes(escapedPropertyName); + int partialConsumed = 0; + while (true) + { + OperationStatus status = JsonWriterHelper.ToUtf8(byteSpan.Slice(partialConsumed), _buffer.Slice(idx), out int consumed, out int written); + idx += written; + if (status == OperationStatus.Done) + { + break; + } + partialConsumed += consumed; + AdvanceAndGrow(ref idx); + } + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Quote; + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.KeyValueSeperator; + + return idx; + } + + private int WritePropertyNameIndented(ReadOnlySpan escapedPropertyName) + { + int idx = 0; + if (_currentDepth < 0) + { + if (_buffer.Length <= idx) + { + GrowAndEnsure(); + } + _buffer[idx++] = JsonConstants.ListSeparator; + } + + if (_tokenType != JsonTokenType.None) + WriteNewLine(ref idx); + + int indent = Indentation; + while (true) + { + bool result = JsonWriterHelper.TryWriteIndentation(_buffer.Slice(idx), indent, out int bytesWritten); + idx += bytesWritten; + if (result) + { + break; + } + indent -= bytesWritten; + AdvanceAndGrow(ref idx); + } + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Quote; + + ReadOnlySpan byteSpan = MemoryMarshal.AsBytes(escapedPropertyName); + int partialConsumed = 0; + while (true) + { + OperationStatus status = JsonWriterHelper.ToUtf8(byteSpan.Slice(partialConsumed), _buffer.Slice(idx), out int consumed, out int written); + idx += written; + if (status == OperationStatus.Done) + { + break; + } + partialConsumed += consumed; + AdvanceAndGrow(ref idx); + } + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Quote; + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.KeyValueSeperator; + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Space; + + return idx; + } + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Literal.cs b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Literal.cs new file mode 100644 index 000000000000..13f3a1671aca --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Literal.cs @@ -0,0 +1,309 @@ +// 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.Diagnostics; + +namespace System.Text.Json +{ + public ref partial struct Utf8JsonWriter + { + /// + /// Writes the property name and the JSON literal "null" as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteNull(string propertyName, bool escape = true) + => WriteNull(propertyName.AsSpan(), escape); + + /// + /// Writes the property name and the JSON literal "null" as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteNull(ReadOnlySpan propertyName, bool escape = true) + { + JsonWriterHelper.ValidateProperty(propertyName); + + ReadOnlySpan span = JsonConstants.NullValue; + + if (escape) + { + WriteLiteralEscape(propertyName, span); + } + else + { + WriteLiteralByOptions(propertyName, span); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.Null; + } + + /// + /// Writes the property name and the JSON literal "null" as part of a name/value pair of a JSON object. + /// + /// The UTF-8 encoded property name of the JSON object to be written. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteNull(ReadOnlySpan utf8PropertyName, bool escape = true) + { + JsonWriterHelper.ValidateProperty(utf8PropertyName); + + ReadOnlySpan span = JsonConstants.NullValue; + + if (escape) + { + WriteLiteralEscape(utf8PropertyName, span); + } + else + { + WriteLiteralByOptions(utf8PropertyName, span); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.Null; + } + + /// + /// Writes the property name and value (as a JSON literal "true" or "false") as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The value to be written as a JSON literal "true" or "false" as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteBoolean(string propertyName, bool value, bool escape = true) + => WriteBoolean(propertyName.AsSpan(), value, escape); + + /// + /// Writes the property name and value (as a JSON literal "true" or "false") as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The value to be written as a JSON literal "true" or "false" as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteBoolean(ReadOnlySpan propertyName, bool value, bool escape = true) + { + JsonWriterHelper.ValidateProperty(propertyName); + + ReadOnlySpan span = value ? JsonConstants.TrueValue : JsonConstants.FalseValue; + + if (escape) + { + WriteLiteralEscape(propertyName, span); + } + else + { + WriteLiteralByOptions(propertyName, span); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = value ? JsonTokenType.True : JsonTokenType.False; + } + + /// + /// Writes the property name and value (as a JSON literal "true" or "false") as part of a name/value pair of a JSON object. + /// + /// The UTF-8 encoded property name of the JSON object to be written. + /// The value to be written as a JSON literal "true" or "false" as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteBoolean(ReadOnlySpan utf8PropertyName, bool value, bool escape = true) + { + JsonWriterHelper.ValidateProperty(utf8PropertyName); + + ReadOnlySpan span = value ? JsonConstants.TrueValue : JsonConstants.FalseValue; + + if (escape) + { + WriteLiteralEscape(utf8PropertyName, span); + } + else + { + WriteLiteralByOptions(utf8PropertyName, span); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = value ? JsonTokenType.True : JsonTokenType.False; + } + + private void WriteLiteralEscape(ReadOnlySpan propertyName, ReadOnlySpan value) + { + int propertyIdx = JsonWriterHelper.NeedsEscaping(propertyName); + + Debug.Assert(propertyIdx >= -1 && propertyIdx < int.MaxValue / 2); + + if (propertyIdx != -1) + { + WriteLiteralEscapeProperty(propertyName, value, propertyIdx); + } + else + { + WriteLiteralByOptions(propertyName, value); + } + } + + private void WriteLiteralEscape(ReadOnlySpan utf8PropertyName, ReadOnlySpan value) + { + int propertyIdx = JsonWriterHelper.NeedsEscaping(utf8PropertyName); + + Debug.Assert(propertyIdx >= -1 && propertyIdx < int.MaxValue / 2); + + if (propertyIdx != -1) + { + WriteLiteralEscapeProperty(utf8PropertyName, value, propertyIdx); + } + else + { + WriteLiteralByOptions(utf8PropertyName, value); + } + } + + private void WriteLiteralEscapeProperty(ReadOnlySpan propertyName, ReadOnlySpan value, int firstEscapeIndexProp) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= propertyName.Length); + Debug.Assert(firstEscapeIndexProp >= 0 && firstEscapeIndexProp < propertyName.Length); + + char[] propertyArray = null; + + int length = JsonWriterHelper.GetMaxEscapedLength(propertyName.Length, firstEscapeIndexProp); + Span escapedPropertyName; + if (length > StackallocThreshold) + { + propertyArray = ArrayPool.Shared.Rent(length); + escapedPropertyName = propertyArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + char* ptr = stackalloc char[length]; + escapedPropertyName = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(propertyName, escapedPropertyName, firstEscapeIndexProp, out int written); + + WriteLiteralByOptions(escapedPropertyName.Slice(0, written), value); + + if (propertyArray != null) + { + ArrayPool.Shared.Return(propertyArray); + } + } + + private void WriteLiteralEscapeProperty(ReadOnlySpan utf8PropertyName, ReadOnlySpan value, int firstEscapeIndexProp) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= utf8PropertyName.Length); + Debug.Assert(firstEscapeIndexProp >= 0 && firstEscapeIndexProp < utf8PropertyName.Length); + + byte[] propertyArray = null; + + int length = JsonWriterHelper.GetMaxEscapedLength(utf8PropertyName.Length, firstEscapeIndexProp); + Span escapedPropertyName; + if (length > StackallocThreshold) + { + propertyArray = ArrayPool.Shared.Rent(length); + escapedPropertyName = propertyArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + byte* ptr = stackalloc byte[length]; + escapedPropertyName = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written); + + WriteLiteralByOptions(escapedPropertyName.Slice(0, written), value); + + if (propertyArray != null) + { + ArrayPool.Shared.Return(propertyArray); + } + } + + private void WriteLiteralByOptions(ReadOnlySpan propertyName, ReadOnlySpan value) + { + ValidateWritingProperty(); + int idx; + if (_writerOptions.Indented) + { + idx = WritePropertyNameIndented(propertyName); + } + else + { + idx = WritePropertyNameMinimized(propertyName); + } + + if (value.Length > _buffer.Length - idx) + { + AdvanceAndGrow(ref idx, value.Length); + } + + value.CopyTo(_buffer.Slice(idx)); + idx += value.Length; + + Advance(idx); + } + + private void WriteLiteralByOptions(ReadOnlySpan utf8PropertyName, ReadOnlySpan value) + { + ValidateWritingProperty(); + int idx; + if (_writerOptions.Indented) + { + idx = WritePropertyNameIndented(utf8PropertyName); + } + else + { + idx = WritePropertyNameMinimized(utf8PropertyName); + } + + if (value.Length > _buffer.Length - idx) + { + AdvanceAndGrow(ref idx, value.Length); + } + + value.CopyTo(_buffer.Slice(idx)); + idx += value.Length; + + Advance(idx); + } + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.SignedNumber.cs b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.SignedNumber.cs new file mode 100644 index 000000000000..be106a111daf --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.SignedNumber.cs @@ -0,0 +1,320 @@ +// 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.Buffers.Text; +using System.Diagnostics; + +namespace System.Text.Json +{ + public ref partial struct Utf8JsonWriter + { + /// + /// Writes the property name and value (as a JSON number) as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The value to be written as a JSON number as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'), for example: 32767. + /// + public void WriteNumber(string propertyName, long value, bool escape = true) + => WriteNumber(propertyName.AsSpan(), value, escape); + + /// + /// Writes the property name and value (as a JSON number) as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The value to be written as a JSON number as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'), for example: 32767. + /// + public void WriteNumber(ReadOnlySpan propertyName, long value, bool escape = true) + { + JsonWriterHelper.ValidateProperty(propertyName); + + if (escape) + { + WriteNumberEscape(propertyName, value); + } + else + { + WriteNumberByOptions(propertyName, value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.Number; + } + + /// + /// Writes the property name and value (as a JSON number) as part of a name/value pair of a JSON object. + /// + /// The UTF-8 encoded property name of the JSON object to be written. + /// The value to be written as a JSON number as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'), for example: 32767. + /// + public void WriteNumber(ReadOnlySpan utf8PropertyName, long value, bool escape = true) + { + JsonWriterHelper.ValidateProperty(utf8PropertyName); + + if (escape) + { + WriteNumberEscape(utf8PropertyName, value); + } + else + { + WriteNumberByOptions(utf8PropertyName, value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.Number; + } + + /// + /// Writes the property name and value (as a JSON number) as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The value to be written as a JSON number as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'), for example: 32767. + /// + public void WriteNumber(string propertyName, int value, bool escape = true) + => WriteNumber(propertyName.AsSpan(), (long)value, escape); + + /// + /// Writes the property name and value (as a JSON number) as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The value to be written as a JSON number as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'), for example: 32767. + /// + public void WriteNumber(ReadOnlySpan propertyName, int value, bool escape = true) + => WriteNumber(propertyName, (long)value, escape); + + /// + /// Writes the property name and value (as a JSON number) as part of a name/value pair of a JSON object. + /// + /// The UTF-8 encoded property name of the JSON object to be written. + /// The value to be written as a JSON number as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'), for example: 32767. + /// + public void WriteNumber(ReadOnlySpan utf8PropertyName, int value, bool escape = true) + => WriteNumber(utf8PropertyName, (long)value, escape); + + private void WriteNumberEscape(ReadOnlySpan propertyName, long value) + { + int propertyIdx = JsonWriterHelper.NeedsEscaping(propertyName); + + Debug.Assert(propertyIdx >= -1 && propertyIdx < int.MaxValue / 2); + + if (propertyIdx != -1) + { + WriteNumberEscapeProperty(propertyName, value, propertyIdx); + } + else + { + WriteNumberByOptions(propertyName, value); + } + } + + private void WriteNumberEscape(ReadOnlySpan utf8PropertyName, long value) + { + int propertyIdx = JsonWriterHelper.NeedsEscaping(utf8PropertyName); + + Debug.Assert(propertyIdx >= -1 && propertyIdx < int.MaxValue / 2); + + if (propertyIdx != -1) + { + WriteNumberEscapeProperty(utf8PropertyName, value, propertyIdx); + } + else + { + WriteNumberByOptions(utf8PropertyName, value); + } + } + + private void WriteNumberEscapeProperty(ReadOnlySpan propertyName, long value, int firstEscapeIndexProp) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= propertyName.Length); + Debug.Assert(firstEscapeIndexProp >= 0 && firstEscapeIndexProp < propertyName.Length); + + char[] propertyArray = null; + + int length = JsonWriterHelper.GetMaxEscapedLength(propertyName.Length, firstEscapeIndexProp); + Span escapedPropertyName; + if (length > StackallocThreshold) + { + propertyArray = ArrayPool.Shared.Rent(length); + escapedPropertyName = propertyArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + char* ptr = stackalloc char[length]; + escapedPropertyName = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(propertyName, escapedPropertyName, firstEscapeIndexProp, out int written); + + WriteNumberByOptions(escapedPropertyName.Slice(0, written), value); + + if (propertyArray != null) + { + ArrayPool.Shared.Return(propertyArray); + } + } + + private void WriteNumberEscapeProperty(ReadOnlySpan utf8PropertyName, long value, int firstEscapeIndexProp) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= utf8PropertyName.Length); + Debug.Assert(firstEscapeIndexProp >= 0 && firstEscapeIndexProp < utf8PropertyName.Length); + + byte[] propertyArray = null; + + int length = JsonWriterHelper.GetMaxEscapedLength(utf8PropertyName.Length, firstEscapeIndexProp); + Span escapedPropertyName; + if (length > StackallocThreshold) + { + propertyArray = ArrayPool.Shared.Rent(length); + escapedPropertyName = propertyArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + byte* ptr = stackalloc byte[length]; + escapedPropertyName = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written); + + WriteNumberByOptions(escapedPropertyName.Slice(0, written), value); + + if (propertyArray != null) + { + ArrayPool.Shared.Return(propertyArray); + } + } + + private void WriteNumberByOptions(ReadOnlySpan propertyName, long value) + { + ValidateWritingProperty(); + if (_writerOptions.Indented) + { + WriteNumberIndented(propertyName, value); + } + else + { + WriteNumberMinimized(propertyName, value); + } + } + + private void WriteNumberByOptions(ReadOnlySpan utf8PropertyName, long value) + { + ValidateWritingProperty(); + if (_writerOptions.Indented) + { + WriteNumberIndented(utf8PropertyName, value); + } + else + { + WriteNumberMinimized(utf8PropertyName, value); + } + } + + private void WriteNumberMinimized(ReadOnlySpan escapedPropertyName, long value) + { + int idx = WritePropertyNameMinimized(escapedPropertyName); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + + private void WriteNumberMinimized(ReadOnlySpan escapedPropertyName, long value) + { + int idx = WritePropertyNameMinimized(escapedPropertyName); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + + private void WriteNumberIndented(ReadOnlySpan escapedPropertyName, long value) + { + int idx = WritePropertyNameIndented(escapedPropertyName); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + + private void WriteNumberIndented(ReadOnlySpan escapedPropertyName, long value) + { + int idx = WritePropertyNameIndented(escapedPropertyName); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + + private void WriteNumberValueFormatLoop(long value, ref int idx) + { + if (!Utf8Formatter.TryFormat(value, _buffer.Slice(idx), out int bytesWritten)) + { + AdvanceAndGrow(ref idx, JsonConstants.MaximumFormatInt64Length); + bool result = Utf8Formatter.TryFormat(value, _buffer, out bytesWritten); + Debug.Assert(result); + } + idx += bytesWritten; + } + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.String.cs b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.String.cs new file mode 100644 index 000000000000..f90c35b2e56e --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.String.cs @@ -0,0 +1,837 @@ +// 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.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace System.Text.Json +{ + public ref partial struct Utf8JsonWriter + { + /// + /// Writes the property name and string text value (as a JSON string) as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The UTF-16 encoded value to be written as a UTF-8 transcoded JSON string as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// The value is always escaped + /// + /// Thrown when the specified property name or value is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteString(string propertyName, string value, bool escape = true) + => WriteString(propertyName.AsSpan(), value.AsSpan(), escape); + + /// + /// Writes the UTF-16 property name and UTF-16 text value (as a JSON string) as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The UTF-16 encoded value to be written as a UTF-8 transcoded JSON string as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// The value is always escaped + /// + /// Thrown when the specified property name or value is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteString(ReadOnlySpan propertyName, ReadOnlySpan value, bool escape = true) + { + JsonWriterHelper.ValidatePropertyAndValue(propertyName, value); + + if (escape) + { + WriteStringEscape(propertyName, value); + } + else + { + WriteStringDontEscape(propertyName, value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.String; + } + + /// + /// Writes the UTF-8 property name and UTF-8 text value (as a JSON string) as part of a name/value pair of a JSON object. + /// + /// The UTF-8 encoded property name of the JSON object to be written. + /// The UTF-8 encoded value to be written as a JSON string as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// The value is always escaped + /// + /// Thrown when the specified property name or value is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteString(ReadOnlySpan utf8PropertyName, ReadOnlySpan utf8Value, bool escape = true) + { + JsonWriterHelper.ValidatePropertyAndValue(utf8PropertyName, utf8Value); + + if (escape) + { + WriteStringEscape(utf8PropertyName, utf8Value); + } + else + { + WriteStringDontEscape(utf8PropertyName, utf8Value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.String; + } + + /// + /// Writes the property name and UTF-16 text value (as a JSON string) as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The UTF-16 encoded value to be written as a UTF-8 transcoded JSON string as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// The value is always escaped + /// + /// Thrown when the specified property name or value is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteString(string propertyName, ReadOnlySpan value, bool escape = true) + => WriteString(propertyName.AsSpan(), value, escape); + + /// + /// Writes the UTF-8 property name and UTF-16 text value (as a JSON string) as part of a name/value pair of a JSON object. + /// + /// The UTF-8 encoded property name of the JSON object to be written. + /// The UTF-16 encoded value to be written as a UTF-8 transcoded JSON string as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// The value is always escaped + /// + /// Thrown when the specified property name or value is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteString(ReadOnlySpan utf8PropertyName, ReadOnlySpan value, bool escape = true) + { + JsonWriterHelper.ValidatePropertyAndValue(utf8PropertyName, value); + + if (escape) + { + WriteStringEscape(utf8PropertyName, value); + } + else + { + WriteStringDontEscape(utf8PropertyName, value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.String; + } + + /// + /// Writes the property name and UTF-8 text value (as a JSON string) as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The UTF-8 encoded value to be written as a JSON string as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// The value is always escaped + /// + /// Thrown when the specified property name or value is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteString(string propertyName, ReadOnlySpan utf8Value, bool escape = true) + => WriteString(propertyName.AsSpan(), utf8Value, escape); + + /// + /// Writes the UTF-16 property name and UTF-8 text value (as a JSON string) as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The UTF-8 encoded value to be written as a JSON string as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// The value is always escaped + /// + /// Thrown when the specified property name or value is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteString(ReadOnlySpan propertyName, ReadOnlySpan utf8Value, bool escape = true) + { + JsonWriterHelper.ValidatePropertyAndValue(propertyName, utf8Value); + + if (escape) + { + WriteStringEscape(propertyName, utf8Value); + } + else + { + WriteStringDontEscape(propertyName, utf8Value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.String; + } + + /// + /// Writes the UTF-16 property name and string text value (as a JSON string) as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The UTF-16 encoded value to be written as a UTF-8 transcoded JSON string as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// The value is always escaped + /// + /// Thrown when the specified property name or value is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteString(ReadOnlySpan propertyName, string value, bool escape = true) + => WriteString(propertyName, value.AsSpan(), escape); + + /// + /// Writes the UTF-8 property name and string text value (as a JSON string) as part of a name/value pair of a JSON object. + /// + /// The UTF-8 encoded property name of the JSON object to be written. + /// The UTF-16 encoded value to be written as a UTF-8 transcoded JSON string as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// The value is always escaped + /// + /// Thrown when the specified property name or value is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteString(ReadOnlySpan utf8PropertyName, string value, bool escape = true) + => WriteString(utf8PropertyName, value.AsSpan(), escape); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WriteStringDontEscape(ReadOnlySpan escapedPropertyName, ReadOnlySpan value) + { + int valueIdx = JsonWriterHelper.NeedsEscaping(value); + if (valueIdx != -1) + { + WriteStringEscapeValueOnly(escapedPropertyName, value, valueIdx); + } + else + { + WriteStringByOptions(escapedPropertyName, value); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WriteStringDontEscape(ReadOnlySpan escapedPropertyName, ReadOnlySpan utf8Value) + { + int valueIdx = JsonWriterHelper.NeedsEscaping(utf8Value); + if (valueIdx != -1) + { + WriteStringEscapeValueOnly(escapedPropertyName, utf8Value, valueIdx); + } + else + { + WriteStringByOptions(escapedPropertyName, utf8Value); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WriteStringDontEscape(ReadOnlySpan escapedPropertyName, ReadOnlySpan utf8Value) + { + int valueIdx = JsonWriterHelper.NeedsEscaping(utf8Value); + if (valueIdx != -1) + { + WriteStringEscapeValueOnly(escapedPropertyName, utf8Value, valueIdx); + } + else + { + WriteStringByOptions(escapedPropertyName, utf8Value); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WriteStringDontEscape(ReadOnlySpan escapedPropertyName, ReadOnlySpan value) + { + int valueIdx = JsonWriterHelper.NeedsEscaping(value); + if (valueIdx != -1) + { + WriteStringEscapeValueOnly(escapedPropertyName, value, valueIdx); + } + else + { + WriteStringByOptions(escapedPropertyName, value); + } + } + + private void WriteStringEscapeValueOnly(ReadOnlySpan escapedPropertyName, ReadOnlySpan value, int firstEscapeIndex) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= value.Length); + Debug.Assert(firstEscapeIndex >= 0 && firstEscapeIndex < value.Length); + + char[] valueArray = ArrayPool.Shared.Rent(JsonWriterHelper.GetMaxEscapedLength(value.Length, firstEscapeIndex)); + Span escapedValue = valueArray; + JsonWriterHelper.EscapeString(value, escapedValue, firstEscapeIndex, out int written); + + WriteStringByOptions(escapedPropertyName, escapedValue.Slice(0, written)); + + ArrayPool.Shared.Return(valueArray); + } + + private void WriteStringEscapeValueOnly(ReadOnlySpan escapedPropertyName, ReadOnlySpan utf8Value, int firstEscapeIndex) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= utf8Value.Length); + Debug.Assert(firstEscapeIndex >= 0 && firstEscapeIndex < utf8Value.Length); + + byte[] valueArray = ArrayPool.Shared.Rent(JsonWriterHelper.GetMaxEscapedLength(utf8Value.Length, firstEscapeIndex)); + Span escapedValue = valueArray; + JsonWriterHelper.EscapeString(utf8Value, escapedValue, firstEscapeIndex, out int written); + + WriteStringByOptions(escapedPropertyName, escapedValue.Slice(0, written)); + + ArrayPool.Shared.Return(valueArray); + } + + private void WriteStringEscapeValueOnly(ReadOnlySpan escapedPropertyName, ReadOnlySpan utf8Value, int firstEscapeIndex) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= utf8Value.Length); + Debug.Assert(firstEscapeIndex >= 0 && firstEscapeIndex < utf8Value.Length); + + byte[] valueArray = ArrayPool.Shared.Rent(JsonWriterHelper.GetMaxEscapedLength(utf8Value.Length, firstEscapeIndex)); + Span escapedValue = valueArray; + JsonWriterHelper.EscapeString(utf8Value, escapedValue, firstEscapeIndex, out int written); + + WriteStringByOptions(escapedPropertyName, escapedValue.Slice(0, written)); + + ArrayPool.Shared.Return(valueArray); + } + + private void WriteStringEscapeValueOnly(ReadOnlySpan escapedPropertyName, ReadOnlySpan value, int firstEscapeIndex) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= value.Length); + Debug.Assert(firstEscapeIndex >= 0 && firstEscapeIndex < value.Length); + + char[] valueArray = ArrayPool.Shared.Rent(JsonWriterHelper.GetMaxEscapedLength(value.Length, firstEscapeIndex)); + Span escapedValue = valueArray; + JsonWriterHelper.EscapeString(value, escapedValue, firstEscapeIndex, out int written); + + WriteStringByOptions(escapedPropertyName, escapedValue.Slice(0, written)); + + ArrayPool.Shared.Return(valueArray); + } + + private void WriteStringEscape(ReadOnlySpan propertyName, ReadOnlySpan value) + { + int valueIdx = JsonWriterHelper.NeedsEscaping(value); + int propertyIdx = JsonWriterHelper.NeedsEscaping(propertyName); + + Debug.Assert(valueIdx >= -1 && valueIdx < int.MaxValue / 2); + Debug.Assert(propertyIdx >= -1 && propertyIdx < int.MaxValue / 2); + + // Equivalent to: valueIdx != -1 || propertyIdx != -1 + if (valueIdx + propertyIdx != -2) + { + WriteStringEscapePropertyOrValue(propertyName, value, propertyIdx, valueIdx); + } + else + { + WriteStringByOptions(propertyName, value); + } + } + + private void WriteStringEscape(ReadOnlySpan utf8PropertyName, ReadOnlySpan utf8Value) + { + int valueIdx = JsonWriterHelper.NeedsEscaping(utf8Value); + int propertyIdx = JsonWriterHelper.NeedsEscaping(utf8PropertyName); + + Debug.Assert(valueIdx >= -1 && valueIdx < int.MaxValue / 2); + Debug.Assert(propertyIdx >= -1 && propertyIdx < int.MaxValue / 2); + + // Equivalent to: valueIdx != -1 || propertyIdx != -1 + if (valueIdx + propertyIdx != -2) + { + WriteStringEscapePropertyOrValue(utf8PropertyName, utf8Value, propertyIdx, valueIdx); + } + else + { + WriteStringByOptions(utf8PropertyName, utf8Value); + } + } + + private void WriteStringEscape(ReadOnlySpan propertyName, ReadOnlySpan utf8Value) + { + int valueIdx = JsonWriterHelper.NeedsEscaping(utf8Value); + int propertyIdx = JsonWriterHelper.NeedsEscaping(propertyName); + + Debug.Assert(valueIdx >= -1 && valueIdx < int.MaxValue / 2); + Debug.Assert(propertyIdx >= -1 && propertyIdx < int.MaxValue / 2); + + // Equivalent to: valueIdx != -1 || propertyIdx != -1 + if (valueIdx + propertyIdx != -2) + { + WriteStringEscapePropertyOrValue(propertyName, utf8Value, propertyIdx, valueIdx); + } + else + { + WriteStringByOptions(propertyName, utf8Value); + } + } + + private void WriteStringEscape(ReadOnlySpan utf8PropertyName, ReadOnlySpan value) + { + int valueIdx = JsonWriterHelper.NeedsEscaping(value); + int propertyIdx = JsonWriterHelper.NeedsEscaping(utf8PropertyName); + + Debug.Assert(valueIdx >= -1 && valueIdx < int.MaxValue / 2); + Debug.Assert(propertyIdx >= -1 && propertyIdx < int.MaxValue / 2); + + // Equivalent to: valueIdx != -1 || propertyIdx != -1 + if (valueIdx + propertyIdx != -2) + { + WriteStringEscapePropertyOrValue(utf8PropertyName, value, propertyIdx, valueIdx); + } + else + { + WriteStringByOptions(utf8PropertyName, value); + } + } + + private void WriteStringEscapePropertyOrValue(ReadOnlySpan propertyName, ReadOnlySpan value, int firstEscapeIndexProp, int firstEscapeIndexVal) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= value.Length); + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= propertyName.Length); + + char[] valueArray = null; + char[] propertyArray = null; + + if (firstEscapeIndexVal != -1) + { + int length = JsonWriterHelper.GetMaxEscapedLength(value.Length, firstEscapeIndexVal); + + Span escapedValue; + if (length > StackallocThreshold) + { + valueArray = ArrayPool.Shared.Rent(length); + escapedValue = valueArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + char* ptr = stackalloc char[length]; + escapedValue = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(value, escapedValue, firstEscapeIndexVal, out int written); + value = escapedValue.Slice(0, written); + } + + if (firstEscapeIndexProp != -1) + { + int length = JsonWriterHelper.GetMaxEscapedLength(propertyName.Length, firstEscapeIndexProp); + + Span escapedPropertyName; + if (length > StackallocThreshold) + { + propertyArray = ArrayPool.Shared.Rent(length); + escapedPropertyName = propertyArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + char* ptr = stackalloc char[length]; + escapedPropertyName = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(propertyName, escapedPropertyName, firstEscapeIndexProp, out int written); + propertyName = escapedPropertyName.Slice(0, written); + } + + WriteStringByOptions(propertyName, value); + + if (valueArray != null) + { + ArrayPool.Shared.Return(valueArray); + } + + if (propertyArray != null) + { + ArrayPool.Shared.Return(propertyArray); + } + } + + private void WriteStringEscapePropertyOrValue(ReadOnlySpan utf8PropertyName, ReadOnlySpan utf8Value, int firstEscapeIndexProp, int firstEscapeIndexVal) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= utf8Value.Length); + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= utf8PropertyName.Length); + + byte[] valueArray = null; + byte[] propertyArray = null; + + if (firstEscapeIndexVal != -1) + { + int length = JsonWriterHelper.GetMaxEscapedLength(utf8Value.Length, firstEscapeIndexVal); + + Span escapedValue; + if (length > StackallocThreshold) + { + valueArray = ArrayPool.Shared.Rent(length); + escapedValue = valueArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + byte* ptr = stackalloc byte[length]; + escapedValue = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(utf8Value, escapedValue, firstEscapeIndexVal, out int written); + utf8Value = escapedValue.Slice(0, written); + } + + if (firstEscapeIndexProp != -1) + { + int length = JsonWriterHelper.GetMaxEscapedLength(utf8PropertyName.Length, firstEscapeIndexProp); + Span escapedPropertyName; + if (length > StackallocThreshold) + { + propertyArray = ArrayPool.Shared.Rent(length); + escapedPropertyName = propertyArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + byte* ptr = stackalloc byte[length]; + escapedPropertyName = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written); + utf8PropertyName = escapedPropertyName.Slice(0, written); + } + + WriteStringByOptions(utf8PropertyName, utf8Value); + + if (valueArray != null) + { + ArrayPool.Shared.Return(valueArray); + } + + if (propertyArray != null) + { + ArrayPool.Shared.Return(propertyArray); + } + } + + private void WriteStringEscapePropertyOrValue(ReadOnlySpan propertyName, ReadOnlySpan utf8Value, int firstEscapeIndexProp, int firstEscapeIndexVal) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= utf8Value.Length); + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= propertyName.Length); + + byte[] valueArray = null; + char[] propertyArray = null; + + if (firstEscapeIndexVal != -1) + { + int length = JsonWriterHelper.GetMaxEscapedLength(utf8Value.Length, firstEscapeIndexVal); + + Span escapedValue; + if (length > StackallocThreshold) + { + valueArray = ArrayPool.Shared.Rent(length); + escapedValue = valueArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + byte* ptr = stackalloc byte[length]; + escapedValue = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(utf8Value, escapedValue, firstEscapeIndexVal, out int written); + utf8Value = escapedValue.Slice(0, written); + } + + if (firstEscapeIndexProp != -1) + { + int length = JsonWriterHelper.GetMaxEscapedLength(propertyName.Length, firstEscapeIndexProp); + Span escapedPropertyName; + if (length > StackallocThreshold) + { + propertyArray = ArrayPool.Shared.Rent(length); + escapedPropertyName = propertyArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + char* ptr = stackalloc char[length]; + escapedPropertyName = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(propertyName, escapedPropertyName, firstEscapeIndexProp, out int written); + propertyName = escapedPropertyName.Slice(0, written); + } + + WriteStringByOptions(propertyName, utf8Value); + + if (valueArray != null) + { + ArrayPool.Shared.Return(valueArray); + } + + if (propertyArray != null) + { + ArrayPool.Shared.Return(propertyArray); + } + } + + private void WriteStringEscapePropertyOrValue(ReadOnlySpan utf8PropertyName, ReadOnlySpan value, int firstEscapeIndexProp, int firstEscapeIndexVal) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= value.Length); + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= utf8PropertyName.Length); + + char[] valueArray = null; + byte[] propertyArray = null; + + if (firstEscapeIndexVal != -1) + { + int length = JsonWriterHelper.GetMaxEscapedLength(value.Length, firstEscapeIndexVal); + + Span escapedValue; + if (length > StackallocThreshold) + { + valueArray = ArrayPool.Shared.Rent(length); + escapedValue = valueArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + char* ptr = stackalloc char[length]; + escapedValue = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(value, escapedValue, firstEscapeIndexVal, out int written); + value = escapedValue.Slice(0, written); + } + + if (firstEscapeIndexProp != -1) + { + int length = JsonWriterHelper.GetMaxEscapedLength(utf8PropertyName.Length, firstEscapeIndexProp); + Span escapedPropertyName; + if (length > StackallocThreshold) + { + propertyArray = ArrayPool.Shared.Rent(length); + escapedPropertyName = propertyArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + byte* ptr = stackalloc byte[length]; + escapedPropertyName = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written); + utf8PropertyName = escapedPropertyName.Slice(0, written); + } + + WriteStringByOptions(utf8PropertyName, value); + + if (valueArray != null) + { + ArrayPool.Shared.Return(valueArray); + } + + if (propertyArray != null) + { + ArrayPool.Shared.Return(propertyArray); + } + } + + private void WriteStringByOptions(ReadOnlySpan propertyName, ReadOnlySpan value) + { + ValidateWritingProperty(); + if (_writerOptions.Indented) + { + WriteStringIndented(propertyName, value); + } + else + { + WriteStringMinimized(propertyName, value); + } + } + + private void WriteStringByOptions(ReadOnlySpan utf8PropertyName, ReadOnlySpan utf8Value) + { + ValidateWritingProperty(); + if (_writerOptions.Indented) + { + WriteStringIndented(utf8PropertyName, utf8Value); + } + else + { + WriteStringMinimized(utf8PropertyName, utf8Value); + } + } + + private void WriteStringByOptions(ReadOnlySpan propertyName, ReadOnlySpan utf8Value) + { + ValidateWritingProperty(); + if (_writerOptions.Indented) + { + WriteStringIndented(propertyName, utf8Value); + } + else + { + WriteStringMinimized(propertyName, utf8Value); + } + } + + private void WriteStringByOptions(ReadOnlySpan utf8PropertyName, ReadOnlySpan value) + { + ValidateWritingProperty(); + if (_writerOptions.Indented) + { + WriteStringIndented(utf8PropertyName, value); + } + else + { + WriteStringMinimized(utf8PropertyName, value); + } + } + + private void WriteStringMinimized(ReadOnlySpan escapedPropertyName, ReadOnlySpan escapedValue) + { + int idx = WritePropertyNameMinimized(escapedPropertyName); + + WriteStringValue(escapedValue, ref idx); + + Advance(idx); + } + + private void WriteStringMinimized(ReadOnlySpan escapedPropertyName, ReadOnlySpan escapedValue) + { + int idx = WritePropertyNameMinimized(escapedPropertyName); + + WriteStringValue(escapedValue, ref idx); + + Advance(idx); + } + + private void WriteStringMinimized(ReadOnlySpan escapedPropertyName, ReadOnlySpan escapedValue) + { + int idx = WritePropertyNameMinimized(escapedPropertyName); + + WriteStringValue(escapedValue, ref idx); + + Advance(idx); + } + + private void WriteStringMinimized(ReadOnlySpan escapedPropertyName, ReadOnlySpan escapedValue) + { + int idx = WritePropertyNameMinimized(escapedPropertyName); + + WriteStringValue(escapedValue, ref idx); + + Advance(idx); + } + + private void WriteStringIndented(ReadOnlySpan escapedPropertyName, ReadOnlySpan escapedValue) + { + int idx = WritePropertyNameIndented(escapedPropertyName); + + WriteStringValue(escapedValue, ref idx); + + Advance(idx); + } + + private void WriteStringIndented(ReadOnlySpan escapedPropertyName, ReadOnlySpan escapedValue) + { + int idx = WritePropertyNameIndented(escapedPropertyName); + + WriteStringValue(escapedValue, ref idx); + + Advance(idx); + } + + private void WriteStringIndented(ReadOnlySpan escapedPropertyName, ReadOnlySpan escapedValue) + { + int idx = WritePropertyNameIndented(escapedPropertyName); + + WriteStringValue(escapedValue, ref idx); + + Advance(idx); + } + + private void WriteStringIndented(ReadOnlySpan escapedPropertyName, ReadOnlySpan escapedValue) + { + int idx = WritePropertyNameIndented(escapedPropertyName); + + WriteStringValue(escapedValue, ref idx); + + Advance(idx); + } + + private void WriteStringValue(ReadOnlySpan escapedValue, ref int idx) + { + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Quote; + + ReadOnlySpan byteSpan = MemoryMarshal.AsBytes(escapedValue); + int partialConsumed = 0; + while (true) + { + OperationStatus status = JsonWriterHelper.ToUtf8(byteSpan.Slice(partialConsumed), _buffer.Slice(idx), out int consumed, out int written); + idx += written; + if (status == OperationStatus.Done) + { + break; + } + partialConsumed += consumed; + AdvanceAndGrow(ref idx); + } + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Quote; + } + + private void WriteStringValue(ReadOnlySpan escapedValue, ref int idx) + { + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Quote; + + CopyLoop(escapedValue, ref idx); + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Quote; + } + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.UnsignedNumber.cs b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.UnsignedNumber.cs new file mode 100644 index 000000000000..ee85f7d84ee2 --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.UnsignedNumber.cs @@ -0,0 +1,326 @@ +// 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.Buffers.Text; +using System.Diagnostics; + +namespace System.Text.Json +{ + public ref partial struct Utf8JsonWriter + { + /// + /// Writes the property name and value (as a JSON number) as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The value to be written as a JSON number as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'), for example: 32767. + /// + [CLSCompliant(false)] + public void WriteNumber(string propertyName, ulong value, bool escape = true) + => WriteNumber(propertyName.AsSpan(), value, escape); + + /// + /// Writes the property name and value (as a JSON number) as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The value to be written as a JSON number as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'), for example: 32767. + /// + [CLSCompliant(false)] + public void WriteNumber(ReadOnlySpan propertyName, ulong value, bool escape = true) + { + JsonWriterHelper.ValidateProperty(propertyName); + + if (escape) + { + WriteNumberEscape(propertyName, value); + } + else + { + WriteNumberByOptions(propertyName, value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.Number; + } + + /// + /// Writes the property name and value (as a JSON number) as part of a name/value pair of a JSON object. + /// + /// The UTF-8 encoded property name of the JSON object to be written. + /// The value to be written as a JSON number as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'), for example: 32767. + /// + [CLSCompliant(false)] + public void WriteNumber(ReadOnlySpan utf8PropertyName, ulong value, bool escape = true) + { + JsonWriterHelper.ValidateProperty(utf8PropertyName); + + if (escape) + { + WriteNumberEscape(utf8PropertyName, value); + } + else + { + WriteNumberByOptions(utf8PropertyName, value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.Number; + } + + /// + /// Writes the property name and value (as a JSON number) as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The value to be written as a JSON number as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'), for example: 32767. + /// + [CLSCompliant(false)] + public void WriteNumber(string propertyName, uint value, bool escape = true) + => WriteNumber(propertyName.AsSpan(), (ulong)value, escape); + + /// + /// Writes the property name and value (as a JSON number) as part of a name/value pair of a JSON object. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// The value to be written as a JSON number as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'), for example: 32767. + /// + [CLSCompliant(false)] + public void WriteNumber(ReadOnlySpan propertyName, uint value, bool escape = true) + => WriteNumber(propertyName, (ulong)value, escape); + + /// + /// Writes the property name and value (as a JSON number) as part of a name/value pair of a JSON object. + /// + /// The UTF-8 encoded property name of the JSON object to be written. + /// The value to be written as a JSON number as part of the name/value pair. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'), for example: 32767. + /// + [CLSCompliant(false)] + public void WriteNumber(ReadOnlySpan utf8PropertyName, uint value, bool escape = true) + => WriteNumber(utf8PropertyName, (ulong)value, escape); + + private void WriteNumberEscape(ReadOnlySpan propertyName, ulong value) + { + int propertyIdx = JsonWriterHelper.NeedsEscaping(propertyName); + + Debug.Assert(propertyIdx >= -1 && propertyIdx < int.MaxValue / 2); + + if (propertyIdx != -1) + { + WriteNumberEscapeProperty(propertyName, value, propertyIdx); + } + else + { + WriteNumberByOptions(propertyName, value); + } + } + + private void WriteNumberEscape(ReadOnlySpan utf8PropertyName, ulong value) + { + int propertyIdx = JsonWriterHelper.NeedsEscaping(utf8PropertyName); + + Debug.Assert(propertyIdx >= -1 && propertyIdx < int.MaxValue / 2); + + if (propertyIdx != -1) + { + WriteNumberEscapeProperty(utf8PropertyName, value, propertyIdx); + } + else + { + WriteNumberByOptions(utf8PropertyName, value); + } + } + + private void WriteNumberEscapeProperty(ReadOnlySpan propertyName, ulong value, int firstEscapeIndexProp) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= propertyName.Length); + Debug.Assert(firstEscapeIndexProp >= 0 && firstEscapeIndexProp < propertyName.Length); + + char[] propertyArray = null; + + int length = JsonWriterHelper.GetMaxEscapedLength(propertyName.Length, firstEscapeIndexProp); + Span escapedPropertyName; + if (length > StackallocThreshold) + { + propertyArray = ArrayPool.Shared.Rent(length); + escapedPropertyName = propertyArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + char* ptr = stackalloc char[length]; + escapedPropertyName = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(propertyName, escapedPropertyName, firstEscapeIndexProp, out int written); + + WriteNumberByOptions(escapedPropertyName.Slice(0, written), value); + + if (propertyArray != null) + { + ArrayPool.Shared.Return(propertyArray); + } + } + + private void WriteNumberEscapeProperty(ReadOnlySpan utf8PropertyName, ulong value, int firstEscapeIndexProp) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= utf8PropertyName.Length); + Debug.Assert(firstEscapeIndexProp >= 0 && firstEscapeIndexProp < utf8PropertyName.Length); + + byte[] propertyArray = null; + + int length = JsonWriterHelper.GetMaxEscapedLength(utf8PropertyName.Length, firstEscapeIndexProp); + Span escapedPropertyName; + if (length > StackallocThreshold) + { + propertyArray = ArrayPool.Shared.Rent(length); + escapedPropertyName = propertyArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + byte* ptr = stackalloc byte[length]; + escapedPropertyName = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written); + + WriteNumberByOptions(escapedPropertyName.Slice(0, written), value); + + if (propertyArray != null) + { + ArrayPool.Shared.Return(propertyArray); + } + } + + private void WriteNumberByOptions(ReadOnlySpan propertyName, ulong value) + { + ValidateWritingProperty(); + if (_writerOptions.Indented) + { + WriteNumberIndented(propertyName, value); + } + else + { + WriteNumberMinimized(propertyName, value); + } + } + + private void WriteNumberByOptions(ReadOnlySpan utf8PropertyName, ulong value) + { + ValidateWritingProperty(); + if (_writerOptions.Indented) + { + WriteNumberIndented(utf8PropertyName, value); + } + else + { + WriteNumberMinimized(utf8PropertyName, value); + } + } + + private void WriteNumberMinimized(ReadOnlySpan escapedPropertyName, ulong value) + { + int idx = WritePropertyNameMinimized(escapedPropertyName); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + + private void WriteNumberMinimized(ReadOnlySpan escapedPropertyName, ulong value) + { + int idx = WritePropertyNameMinimized(escapedPropertyName); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + + private void WriteNumberIndented(ReadOnlySpan escapedPropertyName, ulong value) + { + int idx = WritePropertyNameIndented(escapedPropertyName); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + + private void WriteNumberIndented(ReadOnlySpan escapedPropertyName, ulong value) + { + int idx = WritePropertyNameIndented(escapedPropertyName); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + + private void WriteNumberValueFormatLoop(ulong value, ref int idx) + { + if (!Utf8Formatter.TryFormat(value, _buffer.Slice(idx), out int bytesWritten)) + { + AdvanceAndGrow(ref idx, JsonConstants.MaximumFormatUInt64Length); + bool result = Utf8Formatter.TryFormat(value, _buffer, out bytesWritten); + Debug.Assert(result); + } + idx += bytesWritten; + } + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Comment.cs b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Comment.cs new file mode 100644 index 000000000000..dc749fa65615 --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Comment.cs @@ -0,0 +1,315 @@ +// 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.Diagnostics; +using System.Runtime.InteropServices; + +namespace System.Text.Json +{ + public ref partial struct Utf8JsonWriter + { + /// + /// Writes the string text value (as a JSON comment). + /// + /// The UTF-16 encoded value to be written as a UTF-8 transcoded JSON comment within /*..*/. + /// If this is set to false, the writer assumes the value is properly escaped and skips the escaping step. + /// + /// Thrown when the specified value is too large. + /// + public void WriteCommentValue(string value, bool escape = true) + => WriteCommentValue(value.AsSpan(), escape); + + /// + /// Writes the UTF-16 text value (as a JSON comment). + /// + /// The UTF-16 encoded value to be written as a UTF-8 transcoded JSON comment within /*..*/. + /// If this is set to false, the writer assumes the value is properly escaped and skips the escaping step. + /// + /// Thrown when the specified value is too large. + /// + public void WriteCommentValue(ReadOnlySpan value, bool escape = true) + { + JsonWriterHelper.ValidateValue(value); + + if (escape) + { + WriteCommentEscape(value); + } + else + { + WriteCommentByOptions(value); + } + } + + private void WriteCommentEscape(ReadOnlySpan value) + { + int valueIdx = JsonWriterHelper.NeedsEscaping(value); + + Debug.Assert(valueIdx >= -1 && valueIdx < int.MaxValue / 2); + + if (valueIdx != -1) + { + WriteCommentEscapeValue(value, valueIdx); + } + else + { + WriteCommentByOptions(value); + } + } + + private void WriteCommentByOptions(ReadOnlySpan value) + { + if (_writerOptions.Indented) + { + WriteCommentIndented(value); + } + else + { + WriteCommentMinimized(value); + } + } + + private void WriteCommentMinimized(ReadOnlySpan escapedValue) + { + int idx = 0; + + WriteCommentValue(escapedValue, ref idx); + + Advance(idx); + } + + private void WriteCommentIndented(ReadOnlySpan escapedValue) + { + int idx = 0; + + if (_tokenType != JsonTokenType.None) + WriteNewLine(ref idx); + + int indent = Indentation; + while (true) + { + bool result = JsonWriterHelper.TryWriteIndentation(_buffer.Slice(idx), indent, out int bytesWritten); + idx += bytesWritten; + if (result) + { + break; + } + indent -= bytesWritten; + AdvanceAndGrow(ref idx); + } + + WriteCommentValue(escapedValue, ref idx); + + Advance(idx); + } + + private void WriteCommentEscapeValue(ReadOnlySpan value, int firstEscapeIndexVal) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= value.Length); + Debug.Assert(firstEscapeIndexVal >= 0 && firstEscapeIndexVal < value.Length); + + char[] valueArray = null; + + int length = JsonWriterHelper.GetMaxEscapedLength(value.Length, firstEscapeIndexVal); + + Span escapedValue; + if (length > StackallocThreshold) + { + valueArray = ArrayPool.Shared.Rent(length); + escapedValue = valueArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + char* ptr = stackalloc char[length]; + escapedValue = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(value, escapedValue, firstEscapeIndexVal, out int written); + + WriteCommentByOptions(escapedValue.Slice(0, written)); + + if (valueArray != null) + { + ArrayPool.Shared.Return(valueArray); + } + } + + /// + /// Writes the UTF-8 text value (as a JSON comment). + /// + /// The UTF-8 encoded value to be written as a JSON comment within /*..*/. + /// If this is set to false, the writer assumes the value is properly escaped and skips the escaping step. + /// + /// Thrown when the specified value is too large. + /// + public void WriteCommentValue(ReadOnlySpan utf8Value, bool escape = true) + { + JsonWriterHelper.ValidateValue(utf8Value); + + if (escape) + { + WriteCommentEscape(utf8Value); + } + else + { + WriteCommentByOptions(utf8Value); + } + } + + private void WriteCommentEscape(ReadOnlySpan utf8Value) + { + int valueIdx = JsonWriterHelper.NeedsEscaping(utf8Value); + + Debug.Assert(valueIdx >= -1 && valueIdx < int.MaxValue / 2); + + if (valueIdx != -1) + { + WriteCommentEscapeValue(utf8Value, valueIdx); + } + else + { + WriteCommentByOptions(utf8Value); + } + } + + private void WriteCommentByOptions(ReadOnlySpan utf8Value) + { + if (_writerOptions.Indented) + { + WriteCommentIndented(utf8Value); + } + else + { + WriteCommentMinimized(utf8Value); + } + } + + private void WriteCommentMinimized(ReadOnlySpan escapedValue) + { + int idx = 0; + + WriteCommentValue(escapedValue, ref idx); + + Advance(idx); + } + + private void WriteCommentIndented(ReadOnlySpan escapedValue) + { + int idx = 0; + WriteFormattingPreamble(ref idx); + + WriteCommentValue(escapedValue, ref idx); + + Advance(idx); + } + + private void WriteCommentEscapeValue(ReadOnlySpan utf8Value, int firstEscapeIndexVal) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= utf8Value.Length); + Debug.Assert(firstEscapeIndexVal >= 0 && firstEscapeIndexVal < utf8Value.Length); + + byte[] valueArray = null; + + int length = JsonWriterHelper.GetMaxEscapedLength(utf8Value.Length, firstEscapeIndexVal); + + Span escapedValue; + if (length > StackallocThreshold) + { + valueArray = ArrayPool.Shared.Rent(length); + escapedValue = valueArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + byte* ptr = stackalloc byte[length]; + escapedValue = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(utf8Value, escapedValue, firstEscapeIndexVal, out int written); + + WriteCommentByOptions(escapedValue.Slice(0, written)); + + if (valueArray != null) + { + ArrayPool.Shared.Return(valueArray); + } + } + + private void WriteCommentValue(ReadOnlySpan escapedValue, ref int idx) + { + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Slash; + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Asterisk; + + ReadOnlySpan byteSpan = MemoryMarshal.AsBytes(escapedValue); + int partialConsumed = 0; + while (true) + { + OperationStatus status = JsonWriterHelper.ToUtf8(byteSpan.Slice(partialConsumed), _buffer.Slice(idx), out int consumed, out int written); + idx += written; + if (status == OperationStatus.Done) + { + break; + } + partialConsumed += consumed; + AdvanceAndGrow(ref idx); + } + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Asterisk; + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Slash; + } + + private void WriteCommentValue(ReadOnlySpan escapedValue, ref int idx) + { + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Slash; + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Asterisk; + + CopyLoop(escapedValue, ref idx); + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Asterisk; + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.Slash; + } + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.DateTime.cs b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.DateTime.cs new file mode 100644 index 000000000000..d68d46144e85 --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.DateTime.cs @@ -0,0 +1,56 @@ +// 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; + +namespace System.Text.Json +{ + public ref partial struct Utf8JsonWriter + { + /// + /// Writes the value (as a JSON string) as an element of a JSON array. + /// + /// The value to be written as a JSON string as an element of a JSON array. + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the round-trippable ('O') , for example: 2017-06-12T05:30:45.7680000. + /// + public void WriteStringValue(DateTime value) + { + ValidateWritingValue(); + if (_writerOptions.Indented) + { + WriteStringValueIndented(value); + } + else + { + WriteStringValueMinimized(value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.String; + } + + private void WriteStringValueMinimized(DateTime value) + { + int idx = 0; + WriteListSeparator(ref idx); + + WriteStringValue(value, ref idx); + + Advance(idx); + } + + private void WriteStringValueIndented(DateTime value) + { + int idx = WriteCommaAndFormattingPreamble(); + + WriteStringValue(value, ref idx); + + Advance(idx); + } + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.DateTimeOffset.cs b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.DateTimeOffset.cs new file mode 100644 index 000000000000..3edbf641d7e5 --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.DateTimeOffset.cs @@ -0,0 +1,56 @@ +// 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; + +namespace System.Text.Json +{ + public ref partial struct Utf8JsonWriter + { + /// + /// Writes the value (as a JSON string) as an element of a JSON array. + /// + /// The value to be written as a JSON string as an element of a JSON array. + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the round-trippable ('O') , for example: 2017-06-12T05:30:45.7680000-07:00. + /// + public void WriteStringValue(DateTimeOffset value) + { + ValidateWritingValue(); + if (_writerOptions.Indented) + { + WriteStringValueIndented(value); + } + else + { + WriteStringValueMinimized(value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.String; + } + + private void WriteStringValueMinimized(DateTimeOffset value) + { + int idx = 0; + WriteListSeparator(ref idx); + + WriteStringValue(value, ref idx); + + Advance(idx); + } + + private void WriteStringValueIndented(DateTimeOffset value) + { + int idx = WriteCommaAndFormattingPreamble(); + + WriteStringValue(value, ref idx); + + Advance(idx); + } + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Decimal.cs b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Decimal.cs new file mode 100644 index 000000000000..305cf852ec8a --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Decimal.cs @@ -0,0 +1,56 @@ +// 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; + +namespace System.Text.Json +{ + public ref partial struct Utf8JsonWriter + { + /// + /// Writes the value (as a JSON number) as an element of a JSON array. + /// + /// The value to be written as a JSON number as an element of a JSON array. + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'). + /// + public void WriteNumberValue(decimal value) + { + ValidateWritingValue(); + if (_writerOptions.Indented) + { + WriteNumberValueIndented(value); + } + else + { + WriteNumberValueMinimized(value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.Number; + } + + private void WriteNumberValueMinimized(decimal value) + { + int idx = 0; + WriteListSeparator(ref idx); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + + private void WriteNumberValueIndented(decimal value) + { + int idx = WriteCommaAndFormattingPreamble(); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Double.cs b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Double.cs new file mode 100644 index 000000000000..54fc04680c8b --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Double.cs @@ -0,0 +1,58 @@ +// 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; + +namespace System.Text.Json +{ + public ref partial struct Utf8JsonWriter + { + /// + /// Writes the value (as a JSON number) as an element of a JSON array. + /// + /// The value to be written as a JSON number as an element of a JSON array. + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'). + /// + public void WriteNumberValue(double value) + { + JsonWriterHelper.ValidateDouble(value); + + ValidateWritingValue(); + if (_writerOptions.Indented) + { + WriteNumberValueIndented(value); + } + else + { + WriteNumberValueMinimized(value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.Number; + } + + private void WriteNumberValueMinimized(double value) + { + int idx = 0; + WriteListSeparator(ref idx); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + + private void WriteNumberValueIndented(double value) + { + int idx = WriteCommaAndFormattingPreamble(); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Float.cs b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Float.cs new file mode 100644 index 000000000000..ad8dd12cc77a --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Float.cs @@ -0,0 +1,58 @@ +// 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; + +namespace System.Text.Json +{ + public ref partial struct Utf8JsonWriter + { + /// + /// Writes the value (as a JSON number) as an element of a JSON array. + /// + /// The value to be written as a JSON number as an element of a JSON array. + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'). + /// + public void WriteNumberValue(float value) + { + JsonWriterHelper.ValidateSingle(value); + + ValidateWritingValue(); + if (_writerOptions.Indented) + { + WriteNumberValueIndented(value); + } + else + { + WriteNumberValueMinimized(value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.Number; + } + + private void WriteNumberValueMinimized(float value) + { + int idx = 0; + WriteListSeparator(ref idx); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + + private void WriteNumberValueIndented(float value) + { + int idx = WriteCommaAndFormattingPreamble(); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Guid.cs b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Guid.cs new file mode 100644 index 000000000000..a6d17ce081fa --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Guid.cs @@ -0,0 +1,56 @@ +// 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; + +namespace System.Text.Json +{ + public ref partial struct Utf8JsonWriter + { + /// + /// Writes the value (as a JSON string) as an element of a JSON array. + /// + /// The value to be written as a JSON string as an element of a JSON array. + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'D'), as the form: nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn. + /// + public void WriteStringValue(Guid value) + { + ValidateWritingValue(); + if (_writerOptions.Indented) + { + WriteStringValueIndented(value); + } + else + { + WriteStringValueMinimized(value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.String; + } + + private void WriteStringValueMinimized(Guid value) + { + int idx = 0; + WriteListSeparator(ref idx); + + WriteStringValue(value, ref idx); + + Advance(idx); + } + + private void WriteStringValueIndented(Guid value) + { + int idx = WriteCommaAndFormattingPreamble(); + + WriteStringValue(value, ref idx); + + Advance(idx); + } + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Helpers.cs b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Helpers.cs new file mode 100644 index 000000000000..e59d0a0d1289 --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Helpers.cs @@ -0,0 +1,69 @@ +// 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; + +namespace System.Text.Json +{ + public ref partial struct Utf8JsonWriter + { + private void ValidateWritingValue() + { + if (!_writerOptions.SkipValidation) + { + if (_inObject) + { + Debug.Assert(_tokenType != JsonTokenType.None && _tokenType != JsonTokenType.StartArray); + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.CannotWriteValueWithinObject, tokenType: _tokenType); + } + else + { + if (!_isNotPrimitive && _tokenType != JsonTokenType.None) + { + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.CannotWriteValueAfterPrimitive, tokenType: _tokenType); + } + } + } + } + + private int WriteCommaAndFormattingPreamble() + { + int idx = 0; + WriteListSeparator(ref idx); + WriteFormattingPreamble(ref idx); + return idx; + } + + private void WriteFormattingPreamble(ref int idx) + { + if (_tokenType != JsonTokenType.None) + WriteNewLine(ref idx); + + int indent = Indentation; + while (true) + { + bool result = JsonWriterHelper.TryWriteIndentation(_buffer.Slice(idx), indent, out int bytesWritten); + idx += bytesWritten; + if (result) + { + break; + } + indent -= bytesWritten; + AdvanceAndGrow(ref idx); + } + } + + private void WriteListSeparator(ref int idx) + { + if (_currentDepth < 0) + { + if (_buffer.Length <= idx) + { + GrowAndEnsure(); + } + _buffer[idx++] = JsonConstants.ListSeparator; + } + } + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Literal.cs b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Literal.cs new file mode 100644 index 000000000000..03d57f2823c0 --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Literal.cs @@ -0,0 +1,78 @@ +// 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. + +namespace System.Text.Json +{ + public ref partial struct Utf8JsonWriter + { + /// + /// Writes the JSON literal "null" as an element of a JSON array. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteNullValue() + { + WriteLiteralByOptions(JsonConstants.NullValue); + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.Null; + } + + /// + /// Writes the value (as a JSON literal "true" or "false") as an element of a JSON array. + /// + /// The value to be written as a JSON literal "true" or "false" as an element of a JSON array. + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteBooleanValue(bool value) + { + if (value) + { + WriteLiteralByOptions(JsonConstants.TrueValue); + _tokenType = JsonTokenType.True; + } + else + { + WriteLiteralByOptions(JsonConstants.FalseValue); + _tokenType = JsonTokenType.False; + } + + SetFlagToAddListSeparatorBeforeNextItem(); + } + + private void WriteLiteralByOptions(ReadOnlySpan utf8Value) + { + ValidateWritingValue(); + if (_writerOptions.Indented) + { + WriteLiteralIndented(utf8Value); + } + else + { + WriteLiteralMinimized(utf8Value); + } + } + + private void WriteLiteralMinimized(ReadOnlySpan utf8Value) + { + int idx = 0; + WriteListSeparator(ref idx); + + CopyLoop(utf8Value, ref idx); + + Advance(idx); + } + + private void WriteLiteralIndented(ReadOnlySpan utf8Value) + { + int idx = WriteCommaAndFormattingPreamble(); + + CopyLoop(utf8Value, ref idx); + + Advance(idx); + } + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.SignedNumber.cs b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.SignedNumber.cs new file mode 100644 index 000000000000..d32224db5f0e --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.SignedNumber.cs @@ -0,0 +1,69 @@ +// 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; + +namespace System.Text.Json +{ + public ref partial struct Utf8JsonWriter + { + /// + /// Writes the value (as a JSON number) as an element of a JSON array. + /// + /// The value to be written as a JSON number as an element of a JSON array. + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'), for example: 32767. + /// + public void WriteNumberValue(int value) + => WriteNumberValue((long)value); + + /// + /// Writes the value (as a JSON number) as an element of a JSON array. + /// + /// The value to be written as a JSON number as an element of a JSON array. + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'), for example: 32767. + /// + public void WriteNumberValue(long value) + { + ValidateWritingValue(); + if (_writerOptions.Indented) + { + WriteNumberValueIndented(value); + } + else + { + WriteNumberValueMinimized(value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.Number; + } + + private void WriteNumberValueMinimized(long value) + { + int idx = 0; + WriteListSeparator(ref idx); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + + private void WriteNumberValueIndented(long value) + { + int idx = WriteCommaAndFormattingPreamble(); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.String.cs b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.String.cs new file mode 100644 index 000000000000..e9513bbfbc60 --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.String.cs @@ -0,0 +1,246 @@ +// 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.Diagnostics; + +namespace System.Text.Json +{ + public ref partial struct Utf8JsonWriter + { + /// + /// Writes the string text value (as a JSON string) as an element of a JSON array. + /// + /// The UTF-16 encoded value to be written as a UTF-8 transcoded JSON string element of a JSON array. + /// If this is set to false, the writer assumes the value is properly escaped and skips the escaping step. + /// + /// Thrown when the specified value is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteStringValue(string value, bool escape = true) + => WriteStringValue(value.AsSpan(), escape); + + /// + /// Writes the UTF-16 text value (as a JSON string) as an element of a JSON array. + /// + /// The UTF-16 encoded value to be written as a UTF-8 transcoded JSON string element of a JSON array. + /// If this is set to false, the writer assumes the value is properly escaped and skips the escaping step. + /// + /// Thrown when the specified value is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteStringValue(ReadOnlySpan value, bool escape = true) + { + JsonWriterHelper.ValidateValue(value); + + if (escape) + { + WriteStringEscape(value); + } + else + { + WriteStringByOptions(value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.String; + } + + private void WriteStringEscape(ReadOnlySpan value) + { + int valueIdx = JsonWriterHelper.NeedsEscaping(value); + + Debug.Assert(valueIdx >= -1 && valueIdx < int.MaxValue / 2); + + if (valueIdx != -1) + { + WriteStringEscapeValue(value, valueIdx); + } + else + { + WriteStringByOptions(value); + } + } + + private void WriteStringByOptions(ReadOnlySpan value) + { + ValidateWritingValue(); + if (_writerOptions.Indented) + { + WriteStringIndented(value); + } + else + { + WriteStringMinimized(value); + } + } + + private void WriteStringMinimized(ReadOnlySpan escapedValue) + { + int idx = 0; + WriteListSeparator(ref idx); + + WriteStringValue(escapedValue, ref idx); + + Advance(idx); + } + + private void WriteStringIndented(ReadOnlySpan escapedValue) + { + int idx = WriteCommaAndFormattingPreamble(); + + WriteStringValue(escapedValue, ref idx); + + Advance(idx); + } + + private void WriteStringEscapeValue(ReadOnlySpan value, int firstEscapeIndexVal) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= value.Length); + Debug.Assert(firstEscapeIndexVal >= 0 && firstEscapeIndexVal < value.Length); + + char[] valueArray = null; + + int length = JsonWriterHelper.GetMaxEscapedLength(value.Length, firstEscapeIndexVal); + + Span escapedValue; + if (length > StackallocThreshold) + { + valueArray = ArrayPool.Shared.Rent(length); + escapedValue = valueArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + char* ptr = stackalloc char[length]; + escapedValue = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(value, escapedValue, firstEscapeIndexVal, out int written); + + WriteStringByOptions(escapedValue.Slice(0, written)); + + if (valueArray != null) + { + ArrayPool.Shared.Return(valueArray); + } + } + + /// + /// Writes the UTF-8 text value (as a JSON string) as an element of a JSON array. + /// + /// The UTF-8 encoded value to be written as a JSON string element of a JSON array. + /// If this is set to false, the writer assumes the value is properly escaped and skips the escaping step. + /// + /// Thrown when the specified value is too large. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteStringValue(ReadOnlySpan utf8Value, bool escape = true) + { + JsonWriterHelper.ValidateValue(utf8Value); + + if (escape) + { + WriteStringEscape(utf8Value); + } + else + { + WriteStringByOptions(utf8Value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.String; + } + + private void WriteStringEscape(ReadOnlySpan utf8Value) + { + int valueIdx = JsonWriterHelper.NeedsEscaping(utf8Value); + + Debug.Assert(valueIdx >= -1 && valueIdx < int.MaxValue / 2); + + if (valueIdx != -1) + { + WriteStringEscapeValue(utf8Value, valueIdx); + } + else + { + WriteStringByOptions(utf8Value); + } + } + + private void WriteStringByOptions(ReadOnlySpan utf8Value) + { + ValidateWritingValue(); + if (_writerOptions.Indented) + { + WriteStringIndented(utf8Value); + } + else + { + WriteStringMinimized(utf8Value); + } + } + + private void WriteStringMinimized(ReadOnlySpan escapedValue) + { + int idx = 0; + WriteListSeparator(ref idx); + + WriteStringValue(escapedValue, ref idx); + + Advance(idx); + } + + private void WriteStringIndented(ReadOnlySpan escapedValue) + { + int idx = WriteCommaAndFormattingPreamble(); + + WriteStringValue(escapedValue, ref idx); + + Advance(idx); + } + + private void WriteStringEscapeValue(ReadOnlySpan utf8Value, int firstEscapeIndexVal) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= utf8Value.Length); + Debug.Assert(firstEscapeIndexVal >= 0 && firstEscapeIndexVal < utf8Value.Length); + + byte[] valueArray = null; + + int length = JsonWriterHelper.GetMaxEscapedLength(utf8Value.Length, firstEscapeIndexVal); + + Span escapedValue; + if (length > StackallocThreshold) + { + valueArray = ArrayPool.Shared.Rent(length); + escapedValue = valueArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + byte* ptr = stackalloc byte[length]; + escapedValue = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(utf8Value, escapedValue, firstEscapeIndexVal, out int written); + + WriteStringByOptions(escapedValue.Slice(0, written)); + + if (valueArray != null) + { + ArrayPool.Shared.Return(valueArray); + } + } + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.UnsignedNumber.cs b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.UnsignedNumber.cs new file mode 100644 index 000000000000..107bc0ad50b2 --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.UnsignedNumber.cs @@ -0,0 +1,71 @@ +// 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; + +namespace System.Text.Json +{ + public ref partial struct Utf8JsonWriter + { + /// + /// Writes the value (as a JSON number) as an element of a JSON array. + /// + /// The value to be written as a JSON number as an element of a JSON array. + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'), for example: 32767. + /// + [CLSCompliant(false)] + public void WriteNumberValue(uint value) + => WriteNumberValue((ulong)value); + + /// + /// Writes the value (as a JSON number) as an element of a JSON array. + /// + /// The value to be written as a JSON number as an element of a JSON array. + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + /// + /// Writes the using the default (i.e. 'G'), for example: 32767. + /// + [CLSCompliant(false)] + public void WriteNumberValue(ulong value) + { + ValidateWritingValue(); + if (_writerOptions.Indented) + { + WriteNumberValueIndented(value); + } + else + { + WriteNumberValueMinimized(value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.Number; + } + + private void WriteNumberValueMinimized(ulong value) + { + int idx = 0; + WriteListSeparator(ref idx); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + + private void WriteNumberValueIndented(ulong value) + { + int idx = WriteCommaAndFormattingPreamble(); + + WriteNumberValueFormatLoop(value, ref idx); + + Advance(idx); + } + } +} diff --git a/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs new file mode 100644 index 000000000000..65763c89be42 --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs @@ -0,0 +1,850 @@ +// 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.Diagnostics; +using System.Runtime.CompilerServices; + +namespace System.Text.Json +{ + /// + /// Provides a high-performance API for forward-only, non-cached writing of UTF-8 encoded JSON text. + /// It writes the text sequentially with no caching and adheres to the JSON RFC + /// by default (https://tools.ietf.org/html/rfc8259), with the exception of writing comments. + /// + /// + /// When the user attempts to write invalid JSON and validation is enabled, it throws + /// a with a context specific error message. + /// Since this type is a ref struct, it does not directly support async. However, it does provide + /// support for reentrancy to write partial data, and continue writing in chunks. + /// To be able to format the output with indentation and whitespace OR to skip validation, create an instance of + /// and pass that in to the writer. + /// + public ref partial struct Utf8JsonWriter + { + private const int StackallocThreshold = 256; + private const int DefaultGrowthSize = 4096; + + private readonly IBufferWriter _output; + private int _buffered; + private Span _buffer; + + /// + /// Returns the total amount of bytes written by the so far + /// for the current instance of the . + /// This includes data that has been written beyond what has already been committed. + /// + public long BytesWritten + { + get + { + Debug.Assert(BytesCommitted <= long.MaxValue - _buffered); + return BytesCommitted + _buffered; + } + } + + /// + /// Returns the total amount of bytes committed to the output by the so far + /// for the current instance of the . + /// This is how much the IBufferWriter has advanced. + /// + public long BytesCommitted { get; private set; } + + private bool _inObject; + private bool _isNotPrimitive; + private JsonTokenType _tokenType; + private readonly JsonWriterOptions _writerOptions; + private BitStack _bitStack; + + // The highest order bit of _currentDepth is used to discern whether we are writing the first item in a list or not. + // if (_currentDepth >> 31) == 1, add a list separator before writing the item + // else, no list separator is needed since we are writing the first item. + private int _currentDepth; + + private int Indentation => CurrentDepth * JsonConstants.SpacesPerIndent; + + /// + /// Tracks the recursive depth of the nested objects / arrays within the JSON text + /// written so far. This provides the depth of the current token. + /// + public int CurrentDepth => _currentDepth & JsonConstants.RemoveFlagsBitMask; + + /// + /// Returns the current snapshot of the state which must + /// be captured by the caller and passed back in to the ctor with more data. + /// + /// + /// Thrown when there is JSON data that has been written and buffered but not yet flushed to the . + /// Getting the state for creating a new without first committing the data that has been written + /// would result in an inconsistent state. Call Flush before getting the current state. + /// + /// + /// Unlike the , which is a ref struct, the state can survive + /// across async/await boundaries and hence this type is required to provide support for reading + /// in more data asynchronously before continuing with a new instance of the . + /// + public JsonWriterState GetCurrentState() + { + if (_buffered != 0) + { + throw ThrowHelper.GetInvalidOperationException_CallFlushFirst(_buffered); + } + return new JsonWriterState + { + _bytesWritten = BytesWritten, + _bytesCommitted = BytesCommitted, + _inObject = _inObject, + _isNotPrimitive = _isNotPrimitive, + _tokenType = _tokenType, + _currentDepth = _currentDepth, + _writerOptions = _writerOptions, + _bitStack = _bitStack, + }; + } + + /// + /// Constructs a new instance with a specified . + /// + /// An instance of used as a destination for writing JSON text into. + /// If this is the first call to the ctor, pass in a default state. Otherwise, + /// capture the state from the previous instance of the and pass that back. + /// + /// Thrown when the instance of that is passed in is null. + /// + /// + /// Since this type is a ref struct, it is a stack-only type and all the limitations of ref structs apply to it. + /// This is the reason why the ctor accepts a . + /// + public Utf8JsonWriter(IBufferWriter bufferWriter, JsonWriterState state = default) + { + _output = bufferWriter ?? throw new ArgumentNullException(nameof(bufferWriter)); + _buffered = 0; + BytesCommitted = 0; + _buffer = _output.GetSpan(); + + _inObject = state._inObject; + _isNotPrimitive = state._isNotPrimitive; + _tokenType = state._tokenType; + _writerOptions = state._writerOptions; + _bitStack = state._bitStack; + + _currentDepth = state._currentDepth; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Advance(int count) + { + Debug.Assert(count >= 0 && _buffered <= int.MaxValue - count); + + _buffered += count; + _buffer = _buffer.Slice(count); + } + + /// + /// Advances the underlying based on what has been written so far. + /// + /// Let's the writer know whether more data will be written. This is used to validate + /// that the JSON written so far is structurally valid if no more data is to follow. + /// + /// Thrown when incomplete JSON has been written and is true. + /// (for example when an open object or array needs to be closed). + /// + public void Flush(bool isFinalBlock = true) + { + if (isFinalBlock && !_writerOptions.SkipValidation && (CurrentDepth != 0 || _tokenType == JsonTokenType.None)) + ThrowHelper.ThrowInvalidOperationException_DepthNonZeroOrEmptyJson(_currentDepth); + + Flush(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Flush() + { + _output.Advance(_buffered); + BytesCommitted += _buffered; + _buffered = 0; + } + + /// + /// Writes the beginning of a JSON array. + /// + /// + /// Thrown when the depth of the JSON has exceeded the maximum depth of 1000 + /// OR if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteStartArray() + { + WriteStart(JsonConstants.OpenBracket); + _tokenType = JsonTokenType.StartArray; + } + + /// + /// Writes the beginning of a JSON object. + /// + /// + /// Thrown when the depth of the JSON has exceeded the maximum depth of 1000 + /// OR if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteStartObject() + { + WriteStart(JsonConstants.OpenBrace); + _tokenType = JsonTokenType.StartObject; + } + + private void WriteStart(byte token) + { + if (CurrentDepth >= JsonConstants.MaxWriterDepth) + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.DepthTooLarge, _currentDepth); + + if (_writerOptions.IndentedOrNotSkipValidation) + { + WriteStartSlow(token); + } + else + { + WriteStartMinimized(token); + } + + _currentDepth &= JsonConstants.RemoveFlagsBitMask; + _currentDepth++; + _isNotPrimitive = true; + } + + private void WriteStartMinimized(byte token) + { + int idx = 0; + if (_currentDepth < 0) + { + if (_buffer.Length <= idx) + { + GrowAndEnsure(); + } + _buffer[idx++] = JsonConstants.ListSeparator; + } + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = token; + + Advance(idx); + } + + private void WriteStartSlow(byte token) + { + Debug.Assert(_writerOptions.Indented || !_writerOptions.SkipValidation); + + if (_writerOptions.Indented) + { + if (!_writerOptions.SkipValidation) + { + ValidateStart(); + UpdateBitStackOnStart(token); + } + WriteStartIndented(token); + } + else + { + Debug.Assert(!_writerOptions.SkipValidation); + ValidateStart(); + UpdateBitStackOnStart(token); + WriteStartMinimized(token); + } + } + + private void ValidateStart() + { + if (_inObject) + { + Debug.Assert(_tokenType != JsonTokenType.None && _tokenType != JsonTokenType.StartArray); + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.CannotStartObjectArrayWithoutProperty, tokenType: _tokenType); + } + else + { + Debug.Assert(_tokenType != JsonTokenType.StartObject); + if (_tokenType != JsonTokenType.None && (!_isNotPrimitive || CurrentDepth == 0)) + { + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.CannotStartObjectArrayAfterPrimitiveOrClose, tokenType: _tokenType); + } + } + } + + private void WriteStartIndented(byte token) + { + int idx = 0; + if (_currentDepth < 0) + { + if (_buffer.Length <= idx) + { + GrowAndEnsure(); + } + _buffer[idx++] = JsonConstants.ListSeparator; + } + + if (_tokenType != JsonTokenType.None) + WriteNewLine(ref idx); + + int indent = Indentation; + while (true) + { + bool result = JsonWriterHelper.TryWriteIndentation(_buffer.Slice(idx), indent, out int bytesWritten); + idx += bytesWritten; + if (result) + { + break; + } + indent -= bytesWritten; + AdvanceAndGrow(ref idx); + } + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = token; + + Advance(idx); + } + + /// + /// Writes the beginning of a JSON array with a property name as the key. + /// + /// The UTF-8 encoded property name of the JSON array to be written. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown when the depth of the JSON has exceeded the maximum depth of 1000 + /// OR if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteStartArray(ReadOnlySpan utf8PropertyName, bool escape = true) + { + ValidatePropertyNameAndDepth(utf8PropertyName); + + if (escape) + { + WriteStartEscape(utf8PropertyName, JsonConstants.OpenBracket); + } + else + { + WriteStartByOptions(utf8PropertyName, JsonConstants.OpenBracket); + } + + _currentDepth &= JsonConstants.RemoveFlagsBitMask; + _currentDepth++; + _isNotPrimitive = true; + _tokenType = JsonTokenType.StartArray; + } + + /// + /// Writes the beginning of a JSON object with a property name as the key. + /// + /// The UTF-8 encoded property name of the JSON object to be written. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown when the depth of the JSON has exceeded the maximum depth of 1000 + /// OR if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteStartObject(ReadOnlySpan utf8PropertyName, bool escape = true) + { + ValidatePropertyNameAndDepth(utf8PropertyName); + + if (escape) + { + WriteStartEscape(utf8PropertyName, JsonConstants.OpenBrace); + } + else + { + WriteStartByOptions(utf8PropertyName, JsonConstants.OpenBrace); + } + + _currentDepth &= JsonConstants.RemoveFlagsBitMask; + _currentDepth++; + _isNotPrimitive = true; + _tokenType = JsonTokenType.StartObject; + } + + private void WriteStartEscape(ReadOnlySpan utf8PropertyName, byte token) + { + int propertyIdx = JsonWriterHelper.NeedsEscaping(utf8PropertyName); + + Debug.Assert(propertyIdx >= -1 && propertyIdx < int.MaxValue / 2); + + if (propertyIdx != -1) + { + WriteStartEscapeProperty(utf8PropertyName, token, propertyIdx); + } + else + { + WriteStartByOptions(utf8PropertyName, token); + } + } + + private void WriteStartByOptions(ReadOnlySpan utf8PropertyName, byte token) + { + ValidateWritingProperty(token); + int idx; + if (_writerOptions.Indented) + { + idx = WritePropertyNameIndented(utf8PropertyName); + } + else + { + idx = WritePropertyNameMinimized(utf8PropertyName); + } + + if (1 > _buffer.Length - idx) + { + AdvanceAndGrow(ref idx, 1); + } + + _buffer[idx++] = token; + + Advance(idx); + } + + private void WriteStartEscapeProperty(ReadOnlySpan utf8PropertyName, byte token, int firstEscapeIndexProp) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= utf8PropertyName.Length); + Debug.Assert(firstEscapeIndexProp >= 0 && firstEscapeIndexProp < utf8PropertyName.Length); + + byte[] propertyArray = null; + + int length = JsonWriterHelper.GetMaxEscapedLength(utf8PropertyName.Length, firstEscapeIndexProp); + Span escapedPropertyName; + if (length > StackallocThreshold) + { + propertyArray = ArrayPool.Shared.Rent(length); + escapedPropertyName = propertyArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + byte* ptr = stackalloc byte[length]; + escapedPropertyName = new Span(ptr, length); + } + } + + JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written); + + WriteStartByOptions(escapedPropertyName.Slice(0, written), token); + + if (propertyArray != null) + { + ArrayPool.Shared.Return(propertyArray); + } + } + + /// + /// Writes the beginning of a JSON array with a property name as the key. + /// + /// The UTF-16 encoded property name of the JSON array to be transcoded and written as UTF-8. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown when the depth of the JSON has exceeded the maximum depth of 1000 + /// OR if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteStartArray(string propertyName, bool escape = true) + => WriteStartArray(propertyName.AsSpan(), escape); + + /// + /// Writes the beginning of a JSON object with a property name as the key. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown when the depth of the JSON has exceeded the maximum depth of 1000 + /// OR if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteStartObject(string propertyName, bool escape = true) + => WriteStartObject(propertyName.AsSpan(), escape); + + /// + /// Writes the beginning of a JSON array with a property name as the key. + /// + /// The UTF-16 encoded property name of the JSON array to be transcoded and written as UTF-8. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown when the depth of the JSON has exceeded the maximum depth of 1000 + /// OR if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteStartArray(ReadOnlySpan propertyName, bool escape = true) + { + ValidatePropertyNameAndDepth(propertyName); + + if (escape) + { + WriteStartEscape(propertyName, JsonConstants.OpenBracket); + } + else + { + WriteStartByOptions(propertyName, JsonConstants.OpenBracket); + } + + _currentDepth &= JsonConstants.RemoveFlagsBitMask; + _currentDepth++; + _isNotPrimitive = true; + _tokenType = JsonTokenType.StartArray; + } + + /// + /// Writes the beginning of a JSON object with a property name as the key. + /// + /// The UTF-16 encoded property name of the JSON object to be transcoded and written as UTF-8. + /// If this is set to false, the writer assumes the property name is properly escaped and skips the escaping step. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown when the depth of the JSON has exceeded the maximum depth of 1000 + /// OR if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteStartObject(ReadOnlySpan propertyName, bool escape = true) + { + ValidatePropertyNameAndDepth(propertyName); + + if (escape) + { + WriteStartEscape(propertyName, JsonConstants.OpenBrace); + } + else + { + WriteStartByOptions(propertyName, JsonConstants.OpenBrace); + } + + _currentDepth &= JsonConstants.RemoveFlagsBitMask; + _currentDepth++; + _isNotPrimitive = true; + _tokenType = JsonTokenType.StartObject; + } + + private void WriteStartEscape(ReadOnlySpan propertyName, byte token) + { + int propertyIdx = JsonWriterHelper.NeedsEscaping(propertyName); + + Debug.Assert(propertyIdx >= -1 && propertyIdx < int.MaxValue / 2); + + if (propertyIdx != -1) + { + WriteStartEscapeProperty(propertyName, token, propertyIdx); + } + else + { + WriteStartByOptions(propertyName, token); + } + } + + private void WriteStartByOptions(ReadOnlySpan propertyName, byte token) + { + ValidateWritingProperty(token); + int idx; + if (_writerOptions.Indented) + { + idx = WritePropertyNameIndented(propertyName); + } + else + { + idx = WritePropertyNameMinimized(propertyName); + } + + if (1 > _buffer.Length - idx) + { + AdvanceAndGrow(ref idx, 1); + } + + _buffer[idx++] = token; + + Advance(idx); + } + + private void WriteStartEscapeProperty(ReadOnlySpan propertyName, byte token, int firstEscapeIndexProp) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= propertyName.Length); + Debug.Assert(firstEscapeIndexProp >= 0 && firstEscapeIndexProp < propertyName.Length); + + char[] propertyArray = null; + + int length = JsonWriterHelper.GetMaxEscapedLength(propertyName.Length, firstEscapeIndexProp); + Span escapedPropertyName; + if (length > StackallocThreshold) + { + propertyArray = ArrayPool.Shared.Rent(length); + escapedPropertyName = propertyArray; + } + else + { + // Cannot create a span directly since it gets passed to instance methods on a ref struct. + unsafe + { + char* ptr = stackalloc char[length]; + escapedPropertyName = new Span(ptr, length); + } + } + JsonWriterHelper.EscapeString(propertyName, escapedPropertyName, firstEscapeIndexProp, out int written); + + WriteStartByOptions(escapedPropertyName.Slice(0, written), token); + + if (propertyArray != null) + { + ArrayPool.Shared.Return(propertyArray); + } + } + + /// + /// Writes the end of a JSON array. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteEndArray() + { + WriteEnd(JsonConstants.CloseBracket); + _tokenType = JsonTokenType.EndArray; + } + + /// + /// Writes the end of a JSON object. + /// + /// + /// Thrown if this would result in an invalid JSON to be written (while validation is enabled). + /// + public void WriteEndObject() + { + WriteEnd(JsonConstants.CloseBrace); + _tokenType = JsonTokenType.EndObject; + } + + private void WriteEnd(byte token) + { + if (_writerOptions.IndentedOrNotSkipValidation) + { + WriteEndSlow(token); + } + else + { + WriteEndMinimized(token); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + // Necessary if WriteEndX is called without a corresponding WriteStartX first. + if (CurrentDepth != 0) + { + _currentDepth--; + } + } + + private void WriteEndMinimized(byte token) + { + if (_buffer.Length < 1) + { + GrowAndEnsure(); + } + + _buffer[0] = token; + Advance(1); + } + + private void WriteEndSlow(byte token) + { + Debug.Assert(_writerOptions.Indented || !_writerOptions.SkipValidation); + + if (_writerOptions.Indented) + { + if (!_writerOptions.SkipValidation) + { + ValidateEnd(token); + } + WriteEndIndented(token); + } + else + { + Debug.Assert(!_writerOptions.SkipValidation); + ValidateEnd(token); + WriteEndMinimized(token); + } + } + + private void ValidateEnd(byte token) + { + if (_bitStack.CurrentDepth <= 0) + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.MismatchedObjectArray, token); + + if (token == JsonConstants.CloseBracket) + { + if (_inObject) + { + Debug.Assert(_tokenType != JsonTokenType.None); + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.MismatchedObjectArray, token); + } + } + else + { + Debug.Assert(token == JsonConstants.CloseBrace); + + if (!_inObject) + { + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.MismatchedObjectArray, token); + } + } + + _inObject = _bitStack.Pop(); + } + + private void WriteEndIndented(byte token) + { + // Do not format/indent empty JSON object/array. + if (_tokenType == JsonTokenType.StartObject || _tokenType == JsonTokenType.StartArray) + { + WriteEndMinimized(token); + } + else + { + int idx = 0; + WriteNewLine(ref idx); + + int indent = Indentation; + // Necessary if WriteEndX is called without a corresponding WriteStartX first. + if (indent != 0) + { + // The end token should be at an outer indent and since we haven't updated + // current depth yet, explicitly subtract here. + indent -= JsonConstants.SpacesPerIndent; + } + while (true) + { + bool result = JsonWriterHelper.TryWriteIndentation(_buffer.Slice(idx), indent, out int bytesWritten); + idx += bytesWritten; + if (result) + { + break; + } + indent -= bytesWritten; + AdvanceAndGrow(ref idx); + } + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = token; + + Advance(idx); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WriteNewLine(ref int idx) + { + // Write '\r\n' OR '\n', depending on OS + if (Environment.NewLine.Length == 2) + { + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.CarriageReturn; + } + + if (_buffer.Length <= idx) + { + AdvanceAndGrow(ref idx); + } + _buffer[idx++] = JsonConstants.LineFeed; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void UpdateBitStackOnStart(byte token) + { + if (token == JsonConstants.OpenBracket) + { + _bitStack.PushFalse(); + _inObject = false; + } + else + { + Debug.Assert(token == JsonConstants.OpenBrace); + _bitStack.PushTrue(); + _inObject = true; + } + } + + private void GrowAndEnsure() + { + Flush(); + int previousSpanLength = _buffer.Length; + Debug.Assert(previousSpanLength < DefaultGrowthSize); + _buffer = _output.GetSpan(DefaultGrowthSize); + if (_buffer.Length <= previousSpanLength) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.FailedToGetLargerSpan); + } + } + + private void GrowAndEnsure(int minimumSize) + { + Flush(); + Debug.Assert(minimumSize < DefaultGrowthSize); + _buffer = _output.GetSpan(DefaultGrowthSize); + if (_buffer.Length < minimumSize) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.FailedToGetMinimumSizeSpan, minimumSize); + } + } + + private void AdvanceAndGrow(ref int alreadyWritten) + { + Debug.Assert(alreadyWritten >= 0); + Advance(alreadyWritten); + GrowAndEnsure(); + alreadyWritten = 0; + } + + private void AdvanceAndGrow(ref int alreadyWritten, int minimumSize) + { + Debug.Assert(minimumSize >= 1 && minimumSize <= 128); + Advance(alreadyWritten); + GrowAndEnsure(minimumSize); + alreadyWritten = 0; + } + + private void CopyLoop(ReadOnlySpan span, ref int idx) + { + while (true) + { + if (span.Length <= _buffer.Length - idx) + { + span.CopyTo(_buffer.Slice(idx)); + idx += span.Length; + break; + } + + span.Slice(0, _buffer.Length - idx).CopyTo(_buffer.Slice(idx)); + span = span.Slice(_buffer.Length - idx); + idx = _buffer.Length; + AdvanceAndGrow(ref idx); + } + } + + private void SetFlagToAddListSeparatorBeforeNextItem() + { + _currentDepth |= 1 << 31; + } + } +} diff --git a/src/System.Text.Json/tests/ArrayBufferWriter.cs b/src/System.Text.Json/tests/ArrayBufferWriter.cs new file mode 100644 index 000000000000..13249f80d1c8 --- /dev/null +++ b/src/System.Text.Json/tests/ArrayBufferWriter.cs @@ -0,0 +1,86 @@ +// 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.Runtime.CompilerServices; + +namespace System.Text.Json.Tests +{ + internal class ArrayBufferWriter : IBufferWriter, IDisposable + { + private ResizableArray _buffer; + + public ArrayBufferWriter(int capacity) + { + _buffer = new ResizableArray(ArrayPool.Shared.Rent(capacity)); + } + + public int CommitedByteCount => _buffer.Count; + + public void Clear() + { + _buffer.Count = 0; + } + + public ArraySegment Free => _buffer.Free; + + public ArraySegment Formatted => _buffer.Full; + + public Memory GetMemory(int minimumLength = 0) + { + if (minimumLength < 1) + { + minimumLength = 1; + } + + if (minimumLength > _buffer.FreeCount) + { + int doubleCount = _buffer.FreeCount * 2; + int newSize = minimumLength > doubleCount ? minimumLength : doubleCount; + byte[] newArray = ArrayPool.Shared.Rent(newSize + _buffer.Count); + byte[] oldArray = _buffer.Resize(newArray); + ArrayPool.Shared.Return(oldArray); + } + + return _buffer.FreeMemory; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span GetSpan(int minimumLength = 0) + { + if (minimumLength < 1) + { + minimumLength = 1; + } + + if (minimumLength > _buffer.FreeCount) + { + int doubleCount = _buffer.FreeCount * 2; + int newSize = minimumLength > doubleCount ? minimumLength : doubleCount; + byte[] newArray = ArrayPool.Shared.Rent(newSize + _buffer.Count); + byte[] oldArray = _buffer.Resize(newArray); + ArrayPool.Shared.Return(oldArray); + } + + return _buffer.FreeSpan; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Advance(int bytes) + { + _buffer.Count += bytes; + if (_buffer.Count > _buffer.Capacity) + { + throw new InvalidOperationException("More bytes commited than returned from FreeBuffer"); + } + } + + public void Dispose() + { + byte[] array = _buffer.Array; + _buffer.Array = null; + ArrayPool.Shared.Return(array); + } + } +} diff --git a/src/System.Text.Json/tests/BitStackTests.cs b/src/System.Text.Json/tests/BitStackTests.cs index c4391473a191..41cff4a48964 100644 --- a/src/System.Text.Json/tests/BitStackTests.cs +++ b/src/System.Text.Json/tests/BitStackTests.cs @@ -2,14 +2,13 @@ // 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.Text.Json; using Xunit; -namespace System.Text.JsonTests +namespace System.Text.Json.Tests { public static partial class BitStackTests { - private static Random s_random = new Random(42); + private static readonly Random s_random = new Random(42); [Fact] public static void DefaultBitStack() diff --git a/src/System.Text.Json/tests/FixedSizedBufferWriter.cs b/src/System.Text.Json/tests/FixedSizedBufferWriter.cs new file mode 100644 index 000000000000..22f29ad7b18f --- /dev/null +++ b/src/System.Text.Json/tests/FixedSizedBufferWriter.cs @@ -0,0 +1,44 @@ +// 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.Runtime.CompilerServices; + +namespace System.Text.Json.Tests +{ + internal class FixedSizedBufferWriter : IBufferWriter + { + private readonly byte[] _buffer; + private int _count; + + public FixedSizedBufferWriter(int capacity) + { + _buffer = new byte[capacity]; + } + + public void Clear() + { + _count = 0; + } + + public Span Free => _buffer.AsSpan(_count); + + public Span Formatted => _buffer.AsSpan(0, _count); + + public Memory GetMemory(int minimumLength = 0) => _buffer.AsMemory(_count); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span GetSpan(int minimumLength = 0) => _buffer.AsSpan(_count); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Advance(int bytes) + { + _count += bytes; + if (_count > _buffer.Length) + { + throw new InvalidOperationException("Cannot advance past the end of the buffer."); + } + } + } +} diff --git a/src/System.Text.Json/tests/InvalidBufferWriter.cs b/src/System.Text.Json/tests/InvalidBufferWriter.cs new file mode 100644 index 000000000000..2a92ccb90ec5 --- /dev/null +++ b/src/System.Text.Json/tests/InvalidBufferWriter.cs @@ -0,0 +1,26 @@ +// 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.Runtime.CompilerServices; + +namespace System.Text.Json.Tests +{ + internal class InvalidBufferWriter : IBufferWriter + { + public InvalidBufferWriter() + { + } + + public Memory GetMemory(int minimumLength = 0) => new byte[10]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span GetSpan(int minimumLength = 0) => new byte[10]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Advance(int bytes) + { + } + } +} diff --git a/src/System.Text.Json/tests/JsonWriterStateTests.cs b/src/System.Text.Json/tests/JsonWriterStateTests.cs new file mode 100644 index 000000000000..87a167008f0e --- /dev/null +++ b/src/System.Text.Json/tests/JsonWriterStateTests.cs @@ -0,0 +1,56 @@ +// 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 Xunit; + +namespace System.Text.Json.Tests +{ + public static partial class JsonWriterStateTests + { + [Fact] + public static void DefaultJsonWriterState() + { + JsonWriterState state = default; + Assert.Equal(0, state.BytesCommitted); + Assert.Equal(0, state.BytesWritten); + + var expectedOption = new JsonWriterOptions + { + Indented = false, + SkipValidation = false + }; + Assert.Equal(expectedOption, state.Options); + } + + [Fact] + public static void JsonWriterStateDefaultCtor() + { + var state = new JsonWriterState(); + Assert.Equal(0, state.BytesCommitted); + Assert.Equal(0, state.BytesWritten); + + var expectedOption = new JsonWriterOptions + { + Indented = false, + SkipValidation = false + }; + Assert.Equal(expectedOption, state.Options); + } + + [Fact] + public static void JsonWriterStateCtor() + { + var state = new JsonWriterState(options: default); + Assert.Equal(0, state.BytesCommitted); + Assert.Equal(0, state.BytesWritten); + + var expectedOption = new JsonWriterOptions + { + Indented = false, + SkipValidation = false + }; + Assert.Equal(expectedOption, state.Options); + } + } +} diff --git a/src/System.Text.Json/tests/ResizableArray.cs b/src/System.Text.Json/tests/ResizableArray.cs new file mode 100644 index 000000000000..b02d6b4c4b12 --- /dev/null +++ b/src/System.Text.Json/tests/ResizableArray.cs @@ -0,0 +1,43 @@ +// 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.Runtime.CompilerServices; + +namespace System.Text.Json.Tests +{ + // a List like type designed to be embeded in other types + internal struct ResizableArray + { + public ResizableArray(T[] array, int count = 0) + { + Array = array; + Count = count; + } + + public T[] Array { get; set; } + + public int Count { get; set; } + + public int Capacity => Array.Length; + + public T[] Resize(T[] newArray) + { + T[] oldArray = Array; + Array.AsSpan(0, Count).CopyTo(newArray); // CopyTo will throw if newArray.Length < _count + Array = newArray; + return oldArray; + } + + public ArraySegment Full => new ArraySegment(Array, 0, Count); + + public ArraySegment Free => new ArraySegment(Array, Count, Array.Length - Count); + + public Span FreeSpan => new Span(Array, Count, Array.Length - Count); + + public Memory FreeMemory => new Memory(Array, Count, Array.Length - Count); + + public int FreeCount => Array.Length - Count; + } +} diff --git a/src/System.Text.Json/tests/System.Text.Json.Tests.csproj b/src/System.Text.Json/tests/System.Text.Json.Tests.csproj index 3a181eddcf74..7ca71749e3f6 100644 --- a/src/System.Text.Json/tests/System.Text.Json.Tests.csproj +++ b/src/System.Text.Json/tests/System.Text.Json.Tests.csproj @@ -4,18 +4,24 @@ netcoreapp-Debug;netcoreapp-Release;uap-Windows_NT-Debug;uap-Windows_NT-Release + + + + + + - + diff --git a/src/System.Text.Json/tests/Utf8JsonWriterTests.cs b/src/System.Text.Json/tests/Utf8JsonWriterTests.cs new file mode 100644 index 000000000000..43e78ea69e03 --- /dev/null +++ b/src/System.Text.Json/tests/Utf8JsonWriterTests.cs @@ -0,0 +1,3491 @@ +// 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 Xunit; +using System.IO; +using Newtonsoft.Json; +using System.Globalization; + +namespace System.Text.Json.Tests +{ + public class Utf8JsonWriterTests + { + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void NullCtor(bool formatted, bool skipValidation) + { + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + try + { + var jsonUtf8 = new Utf8JsonWriter(null); + Assert.True(false, "Expected ArgumentNullException to be thrown when null IBufferWriter is passed in."); + } + catch (ArgumentNullException) { } + + try + { + var jsonUtf8 = new Utf8JsonWriter(null, state); + Assert.True(false, "Expected ArgumentNullException to be thrown when null IBufferWriter is passed in."); + } + catch (ArgumentNullException) { } + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void FlushEmpty(bool formatted, bool skipValidation) + { + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + var output = new FixedSizedBufferWriter(0); + try + { + var jsonUtf8 = new Utf8JsonWriter(output, state); + jsonUtf8.Flush(); + WriterDidNotThrow(skipValidation, "Expected InvalidOperationException to be thrown when calling Flush on an empty JSON payload."); + } + catch (InvalidOperationException) { } + + output = new FixedSizedBufferWriter(10); + try + { + var jsonUtf8 = new Utf8JsonWriter(output, state); + jsonUtf8.WriteCommentValue("hi"); + jsonUtf8.Flush(); + WriterDidNotThrow(skipValidation, "Expected InvalidOperationException to be thrown when calling Flush on an empty JSON payload."); + } + catch (InvalidOperationException) { } + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void FlushMultipleTimes(bool formatted, bool skipValidation) + { + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + var output = new FixedSizedBufferWriter(10); + + var jsonUtf8 = new Utf8JsonWriter(output, state); + jsonUtf8.WriteStartObject(); + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + Assert.Equal(2, jsonUtf8.BytesCommitted); + jsonUtf8.Flush(); + Assert.Equal(2, jsonUtf8.BytesCommitted); + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void InvalidBufferWriter(bool formatted, bool skipValidation) + { + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + var output = new InvalidBufferWriter(); + + var jsonUtf8 = new Utf8JsonWriter(output, state); + + try + { + jsonUtf8.WriteNumberValue((ulong)12345678901); + Assert.True(false, "Expected ArgumentException to be thrown when IBufferWriter doesn't have enough space."); + } + catch (ArgumentException) { } + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void FixedSizeBufferWriter_Guid(bool formatted, bool skipValidation) + { + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + var output = new FixedSizedBufferWriter(37); + + var jsonUtf8 = new Utf8JsonWriter(output, state); + + Guid guid = Guid.NewGuid(); + + try + { + jsonUtf8.WriteStringValue(guid); + Assert.True(false, "Expected ArgumentException to be thrown when IBufferWriter doesn't have enough space."); + } + catch (ArgumentException) { } + + output = new FixedSizedBufferWriter(39); + jsonUtf8 = new Utf8JsonWriter(output, state); + jsonUtf8.WriteStringValue(guid); + jsonUtf8.Flush(); + string actualStr = Encoding.UTF8.GetString(output.Formatted); + + Assert.Equal(38, output.Formatted.Length); + Assert.Equal($"\"{guid.ToString()}\"", actualStr); + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void FixedSizeBufferWriter_DateTime(bool formatted, bool skipValidation) + { + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + var output = new FixedSizedBufferWriter(28); + + var jsonUtf8 = new Utf8JsonWriter(output, state); + + var date = new DateTime(2019, 1, 1); + + try + { + jsonUtf8.WriteStringValue(date); + Assert.True(false, "Expected ArgumentException to be thrown when IBufferWriter doesn't have enough space."); + } + catch (ArgumentException) { } + + output = new FixedSizedBufferWriter(29); + jsonUtf8 = new Utf8JsonWriter(output, state); + jsonUtf8.WriteStringValue(date); + jsonUtf8.Flush(); + string actualStr = Encoding.UTF8.GetString(output.Formatted); + + Assert.Equal(29, output.Formatted.Length); + Assert.Equal($"\"{date.ToString("O")}\"", actualStr); + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void FixedSizeBufferWriter_DateTimeOffset(bool formatted, bool skipValidation) + { + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + var output = new FixedSizedBufferWriter(34); + + var jsonUtf8 = new Utf8JsonWriter(output, state); + + DateTimeOffset date = new DateTime(2019, 1, 1); + + try + { + jsonUtf8.WriteStringValue(date); + Assert.True(false, "Expected ArgumentException to be thrown when IBufferWriter doesn't have enough space."); + } + catch (ArgumentException) { } + + output = new FixedSizedBufferWriter(35); + jsonUtf8 = new Utf8JsonWriter(output, state); + jsonUtf8.WriteStringValue(date); + jsonUtf8.Flush(); + string actualStr = Encoding.UTF8.GetString(output.Formatted); + + Assert.Equal(35, output.Formatted.Length); + Assert.Equal($"\"{date.ToString("O")}\"", actualStr); + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void FixedSizeBufferWriter_Decimal(bool formatted, bool skipValidation) + { + var random = new Random(42); + + for (int i = 0; i < 1_000; i++) + { + var output = new FixedSizedBufferWriter(31); + decimal value = JsonTestHelper.NextDecimal(random, 78E14, -78E14); + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + var jsonUtf8 = new Utf8JsonWriter(output, state); + jsonUtf8.WriteNumberValue(value); + + jsonUtf8.Flush(); + string actualStr = Encoding.UTF8.GetString(output.Formatted); + + Assert.True(output.Formatted.Length <= 31); + Assert.Equal(decimal.Parse(actualStr, CultureInfo.InvariantCulture), value); + } + + for (int i = 0; i < 1_000; i++) + { + var output = new FixedSizedBufferWriter(31); + decimal value = JsonTestHelper.NextDecimal(random, 1_000_000, -1_000_000); + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + var jsonUtf8 = new Utf8JsonWriter(output, state); + jsonUtf8.WriteNumberValue(value); + + jsonUtf8.Flush(); + string actualStr = Encoding.UTF8.GetString(output.Formatted); + + Assert.True(output.Formatted.Length <= 31); + Assert.Equal(decimal.Parse(actualStr, CultureInfo.InvariantCulture), value); + } + + { + var output = new FixedSizedBufferWriter(31); + decimal value = 9999999999999999999999999999m; + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + var jsonUtf8 = new Utf8JsonWriter(output, state); + jsonUtf8.WriteNumberValue(value); + + jsonUtf8.Flush(); + string actualStr = Encoding.UTF8.GetString(output.Formatted); + + Assert.Equal(value.ToString().Length, output.Formatted.Length); + Assert.Equal(decimal.Parse(actualStr, CultureInfo.InvariantCulture), value); + } + + { + var output = new FixedSizedBufferWriter(31); + decimal value = -9999999999999999999999999999m; + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + var jsonUtf8 = new Utf8JsonWriter(output, state); + jsonUtf8.WriteNumberValue(value); + + jsonUtf8.Flush(); + string actualStr = Encoding.UTF8.GetString(output.Formatted); + + Assert.Equal(value.ToString().Length, output.Formatted.Length); + Assert.Equal(decimal.Parse(actualStr, CultureInfo.InvariantCulture), value); + } + + { + var output = new FixedSizedBufferWriter(30); + decimal value = -0.9999999999999999999999999999m; + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + try + { + jsonUtf8.WriteNumberValue(value); + Assert.True(false, "Expected ArgumentException to be thrown when IBufferWriter doesn't have enough space."); + } + catch (ArgumentException) { } + + output = new FixedSizedBufferWriter(31); + jsonUtf8 = new Utf8JsonWriter(output, state); + jsonUtf8.WriteNumberValue(value); + + jsonUtf8.Flush(); + string actualStr = Encoding.UTF8.GetString(output.Formatted); + + Assert.Equal(value.ToString().Length, output.Formatted.Length); + Assert.Equal(decimal.Parse(actualStr, CultureInfo.InvariantCulture), value); + } + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void InvalidJsonMismatch(bool formatted, bool skipValidation) + { + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + var output = new ArrayBufferWriter(1024); + + var jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteEndArray(); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteEndObject(); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartArray("property at start", escape: false); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartObject("property at start", escape: false); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartArray(); + jsonUtf8.WriteStartArray("property inside array", escape: false); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartObject(); + jsonUtf8.WriteStartObject(); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartArray(); + jsonUtf8.WriteEndObject(); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartObject(); + jsonUtf8.WriteStringValue("key"); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartArray(); + jsonUtf8.WriteString("key", "value"); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartObject(); + jsonUtf8.WriteEndArray(); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartArray(); + jsonUtf8.WriteStartArray(); + jsonUtf8.WriteEndArray(); + jsonUtf8.WriteEndObject(); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartObject(); + jsonUtf8.WriteStartObject("some object", escape: false); + jsonUtf8.WriteEndObject(); + jsonUtf8.WriteEndArray(); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartArray(); + jsonUtf8.WriteStartObject("some object", escape: false); + jsonUtf8.WriteEndObject(); + jsonUtf8.WriteEndObject(); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartObject(); + jsonUtf8.WriteStartArray("test array", escape: false); + jsonUtf8.WriteEndArray(); + jsonUtf8.WriteEndArray(); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartArray(); + jsonUtf8.WriteEndArray(); + jsonUtf8.WriteEndArray(); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartObject(); + jsonUtf8.WriteEndObject(); + jsonUtf8.WriteEndObject(); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartArray(); + jsonUtf8.WriteStartArray(); + jsonUtf8.WriteEndObject(); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartObject(); + jsonUtf8.WriteStartObject("test object", escape: false); + jsonUtf8.WriteEndArray(); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + output.Dispose(); + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void InvalidJsonIncomplete(bool formatted, bool skipValidation) + { + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + var output = new ArrayBufferWriter(1024); + + var jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartArray(); + jsonUtf8.Flush(isFinalBlock: true); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartObject(); + jsonUtf8.Flush(isFinalBlock: true); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartArray(); + jsonUtf8.WriteStartArray(); + jsonUtf8.WriteEndArray(); + jsonUtf8.Flush(isFinalBlock: true); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartObject(); + jsonUtf8.WriteStartObject("some object", escape: false); + jsonUtf8.Flush(isFinalBlock: true); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartArray(); + jsonUtf8.WriteStartObject("some object", escape: false); + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(isFinalBlock: true); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartObject(); + jsonUtf8.WriteStartArray("test array", escape: false); + jsonUtf8.WriteEndArray(); + jsonUtf8.Flush(isFinalBlock: true); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + output.Dispose(); + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void InvalidJsonPrimitive(bool formatted, bool skipValidation) + { + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + var output = new ArrayBufferWriter(1024); + + var jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteNumberValue(12345); + jsonUtf8.WriteNumberValue(12345); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteNumberValue(12345); + jsonUtf8.WriteStartArray(); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteNumberValue(12345); + jsonUtf8.WriteStartObject(); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteNumberValue(12345); + jsonUtf8.WriteStartArray("property name", escape: false); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteNumberValue(12345); + jsonUtf8.WriteStartObject("property name", escape: false); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteNumberValue(12345); + jsonUtf8.WriteString("property name", "value", escape: false); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteNumberValue(12345); + jsonUtf8.WriteEndArray(); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteNumberValue(12345); + jsonUtf8.WriteEndObject(); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + output.Dispose(); + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void InvalidNumbersJson(bool formatted, bool skipValidation) + { + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + var output = new ArrayBufferWriter(1024); + + var jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteNumberValue(double.NegativeInfinity); + Assert.True(false, "Expected ArgumentException to be thrown for unsupported number values."); + } + catch (ArgumentException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteNumberValue(double.PositiveInfinity); + Assert.True(false, "Expected ArgumentException to be thrown for unsupported number values."); + } + catch (ArgumentException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteNumberValue(double.NaN); + Assert.True(false, "Expected ArgumentException to be thrown for unsupported number values."); + } + catch (ArgumentException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteNumberValue(float.PositiveInfinity); + Assert.True(false, "Expected ArgumentException to be thrown for unsupported number values."); + } + catch (ArgumentException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteNumberValue(float.NegativeInfinity); + Assert.True(false, "Expected ArgumentException to be thrown for unsupported number values."); + } + catch (ArgumentException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteNumberValue(float.NaN); + Assert.True(false, "Expected ArgumentException to be thrown for unsupported number values."); + } + catch (ArgumentException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartObject(); + jsonUtf8.WriteNumber("name", double.NegativeInfinity); + Assert.True(false, "Expected ArgumentException to be thrown for unsupported number values."); + } + catch (ArgumentException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartObject(); + jsonUtf8.WriteNumber("name", double.PositiveInfinity); + Assert.True(false, "Expected ArgumentException to be thrown for unsupported number values."); + } + catch (ArgumentException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartObject(); + jsonUtf8.WriteNumber("name", double.NaN); + Assert.True(false, "Expected ArgumentException to be thrown for unsupported number values."); + } + catch (ArgumentException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartObject(); + jsonUtf8.WriteNumber("name", float.PositiveInfinity); + Assert.True(false, "Expected ArgumentException to be thrown for unsupported number values."); + } + catch (ArgumentException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartObject(); + jsonUtf8.WriteNumber("name", float.NegativeInfinity); + Assert.True(false, "Expected ArgumentException to be thrown for unsupported number values."); + } + catch (ArgumentException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + jsonUtf8.WriteStartObject(); + jsonUtf8.WriteNumber("name", float.NaN); + Assert.True(false, "Expected ArgumentException to be thrown for unsupported number values."); + } + catch (ArgumentException) { } + + output.Dispose(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void InvalidJsonContinueShouldSucceed(bool formatted) + { + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = true }); + + var output = new ArrayBufferWriter(1024); + + var jsonUtf8 = new Utf8JsonWriter(output, state); + + for (int i = 0; i < 100; i++) + jsonUtf8.WriteEndArray(); + jsonUtf8.WriteStartArray(); + jsonUtf8.WriteEndArray(); + jsonUtf8.Flush(); + + ArraySegment arraySegment = output.Formatted; + string actualStr = Encoding.UTF8.GetString(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + + var sb = new StringBuilder(); + for (int i = 0; i < 100; i++) + { + if (formatted) + sb.Append(Environment.NewLine); + sb.Append("]"); + } + sb.Append(","); + if (formatted) + sb.Append(Environment.NewLine); + sb.Append("[]"); + + Assert.Equal(sb.ToString(), actualStr); + + output.Dispose(); + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void WritingTooDeep(bool formatted, bool skipValidation) + { + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + var output = new ArrayBufferWriter(1024); + + var jsonUtf8 = new Utf8JsonWriter(output, state); + try + { + for (int i = 0; i < 1001; i++) + { + jsonUtf8.WriteStartArray(); + } + Assert.True(false, "Expected InvalidOperationException to be thrown for depth >= 1000."); + } + catch (InvalidOperationException) { } + + output.Dispose(); + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void WritingTooDeepProperty(bool formatted, bool skipValidation) + { + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + var output = new ArrayBufferWriter(1024); + + var jsonUtf8 = new Utf8JsonWriter(output, state); + + try + { + jsonUtf8.WriteStartObject(); + for (int i = 0; i < 1000; i++) + { + jsonUtf8.WriteStartArray("name"); + } + Assert.True(false, "Expected InvalidOperationException to be thrown for depth >= 1000."); + } + catch (InvalidOperationException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + + try + { + jsonUtf8.WriteStartObject(); + for (int i = 0; i < 1000; i++) + { + jsonUtf8.WriteStartArray(Encoding.UTF8.GetBytes("name")); + } + Assert.True(false, "Expected InvalidOperationException to be thrown for depth >= 1000."); + } + catch (InvalidOperationException) { } + + output.Dispose(); + } + + [Theory] + [OuterLoop] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void WritingTooLargeProperty(bool formatted, bool skipValidation) + { + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + var output = new ArrayBufferWriter(1024); + + var jsonUtf8 = new Utf8JsonWriter(output, state); + + Span key = new byte[1_000_000_000]; + key.Fill((byte)'a'); + + var keyChars = new char[1_000_000_000]; + keyChars.AsSpan().Fill('a'); + + try + { + jsonUtf8.WriteStartObject(); + jsonUtf8.WriteStartArray(keyChars); + Assert.True(false, "Expected ArgumentException to be thrown for depth >= 1000."); + } + catch (ArgumentException) { } + + jsonUtf8 = new Utf8JsonWriter(output, state); + + try + { + jsonUtf8.WriteStartObject(); + jsonUtf8.WriteStartArray(key); + Assert.True(false, "Expected ArgumentException to be thrown for depth >= 1000."); + } + catch (ArgumentException) { } + + output.Dispose(); + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void WriteSingleValue(bool formatted, bool skipValidation) + { + string expectedStr = "123456789012345"; + + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + for (int i = 0; i < 3; i++) + { + var output = new ArrayBufferWriter(1024); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteNumberValue(123456789012345); + + jsonUtf8.Flush(); + + ArraySegment arraySegment = output.Formatted; + string actualStr = Encoding.UTF8.GetString(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + + Assert.True(expectedStr == actualStr, $"Case: {i}, | Expected: {expectedStr}, | Actual: {actualStr}"); + + output.Dispose(); + } + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void WriteHelloWorld(bool formatted, bool skipValidation) + { + string expectedStr = GetHelloWorldExpectedString(prettyPrint: formatted); + + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + for (int i = 0; i < 9; i++) + { + var output = new ArrayBufferWriter(1024); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartObject(); + + switch (i) + { + case 0: + jsonUtf8.WriteString("message", "Hello, World!", escape: false); + break; + case 1: + jsonUtf8.WriteString("message", "Hello, World!".AsSpan(), escape: false); + break; + case 2: + jsonUtf8.WriteString("message", Encoding.UTF8.GetBytes("Hello, World!"), escape: false); + break; + case 3: + jsonUtf8.WriteString("message".AsSpan(), "Hello, World!", escape: false); + break; + case 4: + jsonUtf8.WriteString("message".AsSpan(), "Hello, World!".AsSpan(), escape: false); + break; + case 5: + jsonUtf8.WriteString("message".AsSpan(), Encoding.UTF8.GetBytes("Hello, World!"), escape: false); + break; + case 6: + jsonUtf8.WriteString(Encoding.UTF8.GetBytes("message"), "Hello, World!", escape: false); + break; + case 7: + jsonUtf8.WriteString(Encoding.UTF8.GetBytes("message"), "Hello, World!".AsSpan(), escape: false); + break; + case 8: + jsonUtf8.WriteString(Encoding.UTF8.GetBytes("message"), Encoding.UTF8.GetBytes("Hello, World!"), escape: false); + break; + } + + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + + ArraySegment arraySegment = output.Formatted; + string actualStr = Encoding.UTF8.GetString(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + + Assert.True(expectedStr == actualStr, $"Case: {i}, | Expected: {expectedStr}, | Actual: {actualStr}"); + + output.Dispose(); + } + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void WritePartialHelloWorld(bool formatted, bool skipValidation) + { + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + var output = new ArrayBufferWriter(10); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartObject(); + + Assert.Equal(0, jsonUtf8.BytesCommitted); + Assert.Equal(1, jsonUtf8.BytesWritten); + + jsonUtf8.WriteString("message", "Hello, World!"); + + Assert.Equal(16, jsonUtf8.BytesCommitted); + if (formatted) + Assert.Equal(26 + 2 + Environment.NewLine.Length + 1, jsonUtf8.BytesWritten); + else + Assert.Equal(26, jsonUtf8.BytesWritten); + + jsonUtf8.Flush(isFinalBlock: false); + + if (formatted) + Assert.Equal(26 + 2 + Environment.NewLine.Length + 1, jsonUtf8.BytesCommitted); // new lines, indentation, white space + else + Assert.Equal(26, jsonUtf8.BytesCommitted); + + Assert.Equal(jsonUtf8.BytesCommitted, jsonUtf8.BytesWritten); + + jsonUtf8.WriteString("message", "Hello, World!"); + jsonUtf8.WriteEndObject(); + + if (formatted) + Assert.Equal(26 + 2 + Environment.NewLine.Length + 1, jsonUtf8.BytesCommitted); + else + Assert.Equal(26, jsonUtf8.BytesCommitted); + + if (formatted) + Assert.Equal(53 + (2 * 2) + (3 * Environment.NewLine.Length) + (1 * 2), jsonUtf8.BytesWritten); // new lines, indentation, white space + else + Assert.Equal(53, jsonUtf8.BytesWritten); + + jsonUtf8.Flush(isFinalBlock: true); + + if (formatted) + Assert.Equal(53 + (2 * 2) + (3 * Environment.NewLine.Length) + (1 * 2), jsonUtf8.BytesCommitted); // new lines, indentation, white space + else + Assert.Equal(53, jsonUtf8.BytesCommitted); + + Assert.Equal(jsonUtf8.BytesCommitted, jsonUtf8.BytesWritten); + + Assert.Equal(0, state.BytesCommitted); + Assert.Equal(0, state.BytesWritten); + + state = jsonUtf8.GetCurrentState(); + Assert.Equal(jsonUtf8.BytesCommitted, state.BytesCommitted); + Assert.Equal(jsonUtf8.BytesWritten, state.BytesWritten); + + output.Dispose(); + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void WritePartialHelloWorldSaveState(bool formatted, bool skipValidation) + { + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + var output = new ArrayBufferWriter(10); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + Assert.Equal(0, jsonUtf8.CurrentDepth); + jsonUtf8.WriteStartObject(); + Assert.Equal(1, jsonUtf8.CurrentDepth); + jsonUtf8.Flush(isFinalBlock: false); + + state = jsonUtf8.GetCurrentState(); + + Assert.Equal(1, state.BytesCommitted); + Assert.Equal(1, state.BytesWritten); + + jsonUtf8 = new Utf8JsonWriter(output, state); + + Assert.Equal(1, jsonUtf8.CurrentDepth); + + jsonUtf8.WriteString("message", "Hello, World!"); + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + + Assert.Equal(jsonUtf8.BytesCommitted, jsonUtf8.BytesWritten); + + if (formatted) + Assert.Equal(26 + 2 + (2 * Environment.NewLine.Length) + 1, jsonUtf8.BytesCommitted); + else + Assert.Equal(26, jsonUtf8.BytesCommitted); + + Assert.Equal(1, state.BytesCommitted); + Assert.Equal(1, state.BytesWritten); + + state = jsonUtf8.GetCurrentState(); + + if (formatted) + { + Assert.Equal(26 + 2 + (2 * Environment.NewLine.Length) + 1, state.BytesCommitted); + Assert.Equal(26 + 2 + (2 * Environment.NewLine.Length) + 1, state.BytesWritten); + } + else + { + Assert.Equal(26, state.BytesCommitted); + Assert.Equal(26, state.BytesWritten); + } + + output.Dispose(); + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void WriteInvalidPartialJson(bool formatted, bool skipValidation) + { + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + var output = new ArrayBufferWriter(10); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartObject(); + + Assert.Equal(0, state.BytesCommitted); + Assert.Equal(0, state.BytesWritten); + + jsonUtf8.Flush(isFinalBlock: false); + + state = jsonUtf8.GetCurrentState(); + + Assert.Equal(1, state.BytesCommitted); + Assert.Equal(1, state.BytesWritten); + + jsonUtf8 = new Utf8JsonWriter(output, state); + + try + { + jsonUtf8.WriteStringValue("Hello, World!"); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + try + { + jsonUtf8.WriteEndArray(); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + output.Dispose(); + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void WritePartialJsonSkipFlush(bool formatted, bool skipValidation) + { + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + var output = new ArrayBufferWriter(10); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartObject(); + jsonUtf8.WriteString("message", "Hello, World!"); + + Assert.Equal(1, jsonUtf8.CurrentDepth); + + try + { + state = jsonUtf8.GetCurrentState(); + Assert.True(false, "Expected InvalidOperationException when trying to get current state without flushing first."); + } + catch (InvalidOperationException) + { + + } + finally + { + jsonUtf8.Flush(isFinalBlock: false); + state = jsonUtf8.GetCurrentState(); + } + + if (formatted) + Assert.Equal(26 + 2 + Environment.NewLine.Length + 1, state.BytesWritten); + else + Assert.Equal(26, state.BytesWritten); + + Assert.Equal(jsonUtf8.BytesWritten, jsonUtf8.BytesCommitted); + + jsonUtf8 = new Utf8JsonWriter(output, state); + Assert.Equal(1, jsonUtf8.CurrentDepth); + Assert.Equal(0, jsonUtf8.BytesWritten); + Assert.Equal(0, jsonUtf8.BytesCommitted); + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + + output.Dispose(); + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void WriteInvalidDepthPartial(bool formatted, bool skipValidation) + { + { + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + var output = new ArrayBufferWriter(10); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartObject(); + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + + Assert.Equal(0, jsonUtf8.CurrentDepth); + + state = jsonUtf8.GetCurrentState(); + + jsonUtf8 = new Utf8JsonWriter(output, state); + + try + { + jsonUtf8.WriteStartObject(); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + output.Dispose(); + } + + { + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + var output = new ArrayBufferWriter(10); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartObject(); + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + + Assert.Equal(0, jsonUtf8.CurrentDepth); + state = jsonUtf8.GetCurrentState(); + + jsonUtf8 = new Utf8JsonWriter(output, state); + + try + { + jsonUtf8.WriteStartObject("name"); + WriterDidNotThrow(skipValidation); + } + catch (InvalidOperationException) { } + + output.Dispose(); + } + } + + [Theory] + [InlineData(true, true, "comment")] + [InlineData(true, false, "comment")] + [InlineData(false, true, "comment")] + [InlineData(false, false, "comment")] + [InlineData(true, true, "comm>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(true, false, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(false, true, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(false, false, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + public void WriteComments(bool formatted, bool skipValidation, string comment) + { + string expectedStr = GetCommentExpectedString(prettyPrint: formatted, comment); + + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + for (int i = 0; i < 3; i++) + { + var output = new ArrayBufferWriter(1024); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartArray(); + + for (int j = 0; j < 10; j++) + { + WriteCommentValue(ref jsonUtf8, i, comment); + } + + switch (i) + { + case 0: + jsonUtf8.WriteStringValue(comment); + break; + case 1: + jsonUtf8.WriteStringValue(comment.AsSpan()); + break; + case 2: + jsonUtf8.WriteStringValue(Encoding.UTF8.GetBytes(comment)); + break; + } + + WriteCommentValue(ref jsonUtf8, i, comment); + + jsonUtf8.WriteEndArray(); + jsonUtf8.Flush(); + + ArraySegment arraySegment = output.Formatted; + string actualStr = Encoding.UTF8.GetString(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + + Assert.True(expectedStr == actualStr, $"Case: {i}, | Expected: {expectedStr}, | Actual: {actualStr}"); + + output.Dispose(); + } + } + + private static void WriteCommentValue(ref Utf8JsonWriter jsonUtf8, int i, string comment) + { + switch (i) + { + case 0: + jsonUtf8.WriteCommentValue(comment, escape: false); + break; + case 1: + jsonUtf8.WriteCommentValue(comment.AsSpan(), escape: false); + break; + case 2: + jsonUtf8.WriteCommentValue(Encoding.UTF8.GetBytes(comment), escape: false); + break; + } + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void WriteStrings(bool formatted, bool skipValidation) + { + string value = "temp"; + string expectedStr = GetStringsExpectedString(prettyPrint: formatted, value); + + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + for (int i = 0; i < 6; i++) + { + var output = new ArrayBufferWriter(1024); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartArray(); + + for (int j = 0; j < 10; j++) + { + switch (i) + { + case 0: + jsonUtf8.WriteStringValue(value); + break; + case 1: + jsonUtf8.WriteStringValue(value.AsSpan()); + break; + case 2: + jsonUtf8.WriteStringValue(Encoding.UTF8.GetBytes(value)); + break; + case 3: + jsonUtf8.WriteStringValue(value, escape: false); + break; + case 4: + jsonUtf8.WriteStringValue(value.AsSpan(), escape: false); + break; + case 5: + jsonUtf8.WriteStringValue(Encoding.UTF8.GetBytes(value), escape: false); + break; + } + } + + jsonUtf8.WriteEndArray(); + jsonUtf8.Flush(); + + ArraySegment arraySegment = output.Formatted; + string actualStr = Encoding.UTF8.GetString(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + + Assert.True(expectedStr == actualStr, $"Case: {i}, | Expected: {expectedStr}, | Actual: {actualStr}"); + + output.Dispose(); + } + } + + [Theory] + [InlineData(true, true, "mess\nage", "Hello, \nWorld!")] + [InlineData(true, false, "mess\nage", "Hello, \nWorld!")] + [InlineData(false, true, "mess\nage", "Hello, \nWorld!")] + [InlineData(false, false, "mess\nage", "Hello, \nWorld!")] + [InlineData(true, true, "message", "Hello, \nWorld!")] + [InlineData(true, false, "message", "Hello, \nWorld!")] + [InlineData(false, true, "message", "Hello, \nWorld!")] + [InlineData(false, false, "message", "Hello, \nWorld!")] + [InlineData(true, true, "mess\nage", "Hello, World!")] + [InlineData(true, false, "mess\nage", "Hello, World!")] + [InlineData(false, true, "mess\nage", "Hello, World!")] + [InlineData(false, false, "mess\nage", "Hello, World!")] + [InlineData(true, true, "message", "Hello, World!")] + [InlineData(true, false, "message", "Hello, World!")] + [InlineData(false, true, "message", "Hello, World!")] + [InlineData(false, false, "message", "Hello, World!")] + [InlineData(true, true, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>mess\nage", ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>Hello, \nWorld!")] + [InlineData(true, false, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>mess\nage", ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>Hello, \nWorld!")] + [InlineData(false, true, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>mess\nage", ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>Hello, \nWorld!")] + [InlineData(false, false, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>mess\nage", ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>Hello, \nWorld!")] + public void WriteHelloWorldEscaped(bool formatted, bool skipValidation, string key, string value) + { + string expectedStr = GetEscapedExpectedString(prettyPrint: formatted, key, value, StringEscapeHandling.EscapeHtml); + string expectedStrNoEscape = GetEscapedExpectedString(prettyPrint: formatted, key, value, StringEscapeHandling.EscapeHtml, escape: false); + + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + for (int i = 0; i < 18; i++) + { + var output = new ArrayBufferWriter(1024); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartObject(); + + switch (i) + { + case 0: + jsonUtf8.WriteString(key, value, escape: true); + break; + case 1: + jsonUtf8.WriteString(key.AsSpan(), value.AsSpan(), escape: true); + break; + case 2: + jsonUtf8.WriteString(Encoding.UTF8.GetBytes(key), Encoding.UTF8.GetBytes(value), escape: true); + break; + case 3: + jsonUtf8.WriteString(key, value.AsSpan(), escape: true); + break; + case 4: + jsonUtf8.WriteString(key, Encoding.UTF8.GetBytes(value), escape: true); + break; + case 5: + jsonUtf8.WriteString(key.AsSpan(), value, escape: true); + break; + case 6: + jsonUtf8.WriteString(key.AsSpan(), Encoding.UTF8.GetBytes(value), escape: true); + break; + case 7: + jsonUtf8.WriteString(Encoding.UTF8.GetBytes(key), value, escape: true); + break; + case 8: + jsonUtf8.WriteString(Encoding.UTF8.GetBytes(key), value.AsSpan(), escape: true); + break; + case 9: + jsonUtf8.WriteString(key, value, escape: false); + break; + case 10: + jsonUtf8.WriteString(key.AsSpan(), value.AsSpan(), escape: false); + break; + case 11: + jsonUtf8.WriteString(Encoding.UTF8.GetBytes(key), Encoding.UTF8.GetBytes(value), escape: false); + break; + case 12: + jsonUtf8.WriteString(key, value.AsSpan(), escape: false); + break; + case 13: + jsonUtf8.WriteString(key, Encoding.UTF8.GetBytes(value), escape: false); + break; + case 14: + jsonUtf8.WriteString(key.AsSpan(), value, escape: false); + break; + case 15: + jsonUtf8.WriteString(key.AsSpan(), Encoding.UTF8.GetBytes(value), escape: false); + break; + case 16: + jsonUtf8.WriteString(Encoding.UTF8.GetBytes(key), value, escape: false); + break; + case 17: + jsonUtf8.WriteString(Encoding.UTF8.GetBytes(key), value.AsSpan(), escape: false); + break; + } + + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + + ArraySegment arraySegment = output.Formatted; + string actualStr = Encoding.UTF8.GetString(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + + if (i >= 9) + Assert.True(expectedStrNoEscape == actualStr, $"Case: {i}, | Expected: {expectedStr}, | Actual: {actualStr}"); + else + Assert.True(expectedStr == actualStr, $"Case: {i}, | Expected: {expectedStr}, | Actual: {actualStr}"); + + output.Dispose(); + } + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void EscapeAsciiCharacters(bool formatted, bool skipValidation) + { + var propertyArray = new char[128]; + + char[] specialCases = { '+', '`', (char)0x7F }; + for (int i = 0; i < propertyArray.Length; i++) + { + if (Array.IndexOf(specialCases, (char)i) != -1) + { + propertyArray[i] = (char)0; + } + else + { + propertyArray[i] = (char)i; + } + } + + string propertyName = new string(propertyArray); + string value = new string(propertyArray); + + string expectedStr = GetEscapedExpectedString(prettyPrint: formatted, propertyName, value, StringEscapeHandling.EscapeHtml); + + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + for (int i = 0; i < 4; i++) + { + var output = new ArrayBufferWriter(1024); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartObject(); + + switch (i) + { + case 0: + jsonUtf8.WriteString(propertyName, value, escape: true); + break; + case 1: + jsonUtf8.WriteString(Encoding.UTF8.GetBytes(propertyName), Encoding.UTF8.GetBytes(value), escape: true); + break; + case 2: + expectedStr = GetEscapedExpectedString(prettyPrint: formatted, propertyName, value, StringEscapeHandling.EscapeHtml, escape: false); + jsonUtf8.WriteString(propertyName, value, escape: false); + break; + case 3: + expectedStr = GetEscapedExpectedString(prettyPrint: formatted, propertyName, value, StringEscapeHandling.EscapeHtml, escape: false); + jsonUtf8.WriteString(Encoding.UTF8.GetBytes(propertyName), Encoding.UTF8.GetBytes(value), escape: false); + break; + } + + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + + ArraySegment arraySegment = output.Formatted; + string actualStr = Encoding.UTF8.GetString(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + + Assert.Equal(expectedStr, actualStr); + + output.Dispose(); + } + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void EscapeCharacters(bool formatted, bool skipValidation) + { + // Do not include surrogate pairs. + var propertyArray = new char[0xD800 + (0xFFFF - 0xE000) + 1]; + + for (int i = 128; i < propertyArray.Length; i++) + { + if (i < 0xD800 || i > 0xDFFF) + { + propertyArray[i] = (char)i; + } + } + + string propertyName = new string(propertyArray); + string value = new string(propertyArray); + + string expectedStr = GetEscapedExpectedString(prettyPrint: formatted, propertyName, value, StringEscapeHandling.EscapeNonAscii); + + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + for (int i = 0; i < 4; i++) + { + var output = new ArrayBufferWriter(1024); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartObject(); + + switch (i) + { + case 0: + jsonUtf8.WriteString(propertyName, value, escape: true); + break; + case 1: + jsonUtf8.WriteString(Encoding.UTF8.GetBytes(propertyName), Encoding.UTF8.GetBytes(value), escape: true); + break; + case 2: + expectedStr = GetEscapedExpectedString(prettyPrint: formatted, propertyName, value, StringEscapeHandling.EscapeNonAscii, escape: false); + jsonUtf8.WriteString(propertyName, value, escape: false); + break; + case 3: + expectedStr = GetEscapedExpectedString(prettyPrint: formatted, propertyName, value, StringEscapeHandling.EscapeNonAscii, escape: false); + jsonUtf8.WriteString(Encoding.UTF8.GetBytes(propertyName), Encoding.UTF8.GetBytes(value), escape: false); + break; + } + + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + + ArraySegment arraySegment = output.Formatted; + string actualStr = Encoding.UTF8.GetString(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + + Assert.Equal(expectedStr, actualStr); + + output.Dispose(); + } + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void EscapeSurrogatePairs(bool formatted, bool skipValidation) + { + var propertyArray = new char[10] { 'a', (char)0xD800, (char)0xDC00, (char)0xD803, (char)0xDE6D, (char)0xD834, (char)0xDD1E, (char)0xDBFF, (char)0xDFFF, 'a' }; + + string propertyName = new string(propertyArray); + string value = new string(propertyArray); + + string expectedStr = GetEscapedExpectedString(prettyPrint: formatted, propertyName, value, StringEscapeHandling.EscapeNonAscii); + + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + for (int i = 0; i < 4; i++) + { + var output = new ArrayBufferWriter(1024); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartObject(); + + switch (i) + { + case 0: + jsonUtf8.WriteString(propertyName, value, escape: true); + break; + case 1: + jsonUtf8.WriteString(Encoding.UTF8.GetBytes(propertyName), Encoding.UTF8.GetBytes(value), escape: true); + break; + case 2: + expectedStr = GetEscapedExpectedString(prettyPrint: formatted, propertyName, value, StringEscapeHandling.EscapeNonAscii, escape: false); + jsonUtf8.WriteString(propertyName, value, escape: false); + break; + case 3: + expectedStr = GetEscapedExpectedString(prettyPrint: formatted, propertyName, value, StringEscapeHandling.EscapeNonAscii, escape: false); + jsonUtf8.WriteString(Encoding.UTF8.GetBytes(propertyName), Encoding.UTF8.GetBytes(value), escape: false); + break; + } + + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + + ArraySegment arraySegment = output.Formatted; + string actualStr = Encoding.UTF8.GetString(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + + Assert.Equal(expectedStr, actualStr); + + output.Dispose(); + } + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void InvalidUTF8(bool formatted, bool skipValidation) + { + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + var output = new ArrayBufferWriter(1024); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartObject(); + for (int i = 0; i < 8; i++) + { + try + { + switch (i) + { + case 0: + jsonUtf8.WriteString(new byte[2] { 0xc3, 0x28 }, new byte[2] { 0xc3, 0x28 }, escape: false); + AssertWriterThrow(noThrow: false); + break; + case 1: + jsonUtf8.WriteString(new byte[2] { 0xc3, 0x28 }, new byte[2] { 0xc3, 0xb1 }, escape: false); + AssertWriterThrow(noThrow: true); + break; + case 2: + jsonUtf8.WriteString(new byte[2] { 0xc3, 0xb1 }, new byte[2] { 0xc3, 0x28 }, escape: false); + AssertWriterThrow(noThrow: false); + break; + case 3: + jsonUtf8.WriteString(new byte[2] { 0xc3, 0xb1 }, new byte[2] { 0xc3, 0xb1 }, escape: false); + AssertWriterThrow(noThrow: true); + break; + case 4: + jsonUtf8.WriteString(new byte[2] { 0xc3, 0x28 }, new byte[2] { 0xc3, 0x28 }, escape: true); + AssertWriterThrow(noThrow: false); + break; + case 5: + jsonUtf8.WriteString(new byte[2] { 0xc3, 0x28 }, new byte[2] { 0xc3, 0xb1 }, escape: true); + AssertWriterThrow(noThrow: false); + break; + case 6: + jsonUtf8.WriteString(new byte[2] { 0xc3, 0xb1 }, new byte[2] { 0xc3, 0x28 }, escape: true); + AssertWriterThrow(noThrow: false); + break; + case 7: + jsonUtf8.WriteString(new byte[2] { 0xc3, 0xb1 }, new byte[2] { 0xc3, 0xb1 }, escape: true); + AssertWriterThrow(noThrow: true); + break; + } + } + catch (ArgumentException) { } + } + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + + output.Dispose(); + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void WriteCustomStrings(bool formatted, bool skipValidation) + { + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + var output = new ArrayBufferWriter(10); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartObject(); + + for (int i = 0; i < 1_000; i++) + jsonUtf8.WriteString("message", "Hello, World!", escape: false); + + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + + ArraySegment arraySegment = output.Formatted; + string actualStr = Encoding.UTF8.GetString(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + + Assert.Equal(GetCustomExpectedString(formatted), actualStr); + + output.Dispose(); + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void WriteStartEnd(bool formatted, bool skipValidation) + { + string expectedStr = GetStartEndExpectedString(prettyPrint: formatted); + + var output = new ArrayBufferWriter(1024); + + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartArray(); + jsonUtf8.WriteStartObject(); + jsonUtf8.WriteEndObject(); + jsonUtf8.WriteEndArray(); + jsonUtf8.Flush(); + + ArraySegment arraySegment = output.Formatted; + string actualStr = Encoding.UTF8.GetString(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + + Assert.Equal(expectedStr, actualStr); + + output.Dispose(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void WriteStartEndInvalid(bool formatted) + { + { + string expectedStr = "[}"; + + var output = new ArrayBufferWriter(1024); + + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = true }); + + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartArray(); + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + + ArraySegment arraySegment = output.Formatted; + string actualStr = Encoding.UTF8.GetString(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + + Assert.Equal(expectedStr, actualStr); + + output.Dispose(); + } + + { + string expectedStr = "{]"; + + var output = new ArrayBufferWriter(1024); + + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = true }); + + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartObject(); + jsonUtf8.WriteEndArray(); + jsonUtf8.Flush(); + + ArraySegment arraySegment = output.Formatted; + string actualStr = Encoding.UTF8.GetString(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + + Assert.Equal(expectedStr, actualStr); + + output.Dispose(); + } + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void WriteStartEndWithPropertyNameArray(bool formatted, bool skipValidation) + { + string expectedStr = GetStartEndWithPropertyArrayExpectedString(prettyPrint: formatted); + + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + for (int i = 0; i < 6; i++) + { + var output = new ArrayBufferWriter(1024); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartObject(); + + switch (i) + { + case 0: + jsonUtf8.WriteStartArray("property name", escape: false); + break; + case 1: + jsonUtf8.WriteStartArray("property name".AsSpan(), escape: false); + break; + case 2: + jsonUtf8.WriteStartArray(Encoding.UTF8.GetBytes("property name"), escape: false); + break; + case 3: + jsonUtf8.WriteStartArray("property name", escape: true); + break; + case 4: + jsonUtf8.WriteStartArray("property name".AsSpan(), escape: true); + break; + case 5: + jsonUtf8.WriteStartArray(Encoding.UTF8.GetBytes("property name"), escape: true); + break; + } + + jsonUtf8.WriteEndArray(); + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + + ArraySegment arraySegment = output.Formatted; + string actualStr = Encoding.UTF8.GetString(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + + Assert.Equal(expectedStr, actualStr); + + output.Dispose(); + } + } + + [Theory] + [InlineData(true, true, 10)] + [InlineData(true, false, 10)] + [InlineData(false, true, 10)] + [InlineData(false, false, 10)] + [InlineData(true, true, 100)] + [InlineData(true, false, 100)] + [InlineData(false, true, 100)] + [InlineData(false, false, 100)] + public void WriteStartEndWithPropertyNameArray(bool formatted, bool skipValidation, int keyLength) + { + var keyChars = new char[keyLength]; + for (int i = 0; i < keyChars.Length; i++) + { + keyChars[i] = '<'; + } + var key = new string(keyChars); + + string expectedStr = GetStartEndWithPropertyArrayExpectedString(key, prettyPrint: formatted, escape: true); + string expectedStrNoEscape = GetStartEndWithPropertyArrayExpectedString(key, prettyPrint: formatted, escape: false); + + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + for (int i = 0; i < 6; i++) + { + var output = new ArrayBufferWriter(1024); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartObject(); + + switch (i) + { + case 0: + jsonUtf8.WriteStartArray(key, escape: false); + break; + case 1: + jsonUtf8.WriteStartArray(key.AsSpan(), escape: false); + break; + case 2: + jsonUtf8.WriteStartArray(Encoding.UTF8.GetBytes(key), escape: false); + break; + case 3: + jsonUtf8.WriteStartArray(key, escape: true); + break; + case 4: + jsonUtf8.WriteStartArray(key.AsSpan(), escape: true); + break; + case 5: + jsonUtf8.WriteStartArray(Encoding.UTF8.GetBytes(key), escape: true); + break; + } + + jsonUtf8.WriteEndArray(); + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + + ArraySegment arraySegment = output.Formatted; + string actualStr = Encoding.UTF8.GetString(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + + if (i < 3) + Assert.Equal(expectedStrNoEscape, actualStr); + else + Assert.Equal(expectedStr, actualStr); + + output.Dispose(); + } + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void WriteStartEndWithPropertyNameObject(bool formatted, bool skipValidation) + { + string expectedStr = GetStartEndWithPropertyObjectExpectedString(prettyPrint: formatted); + + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + for (int i = 0; i < 6; i++) + { + var output = new ArrayBufferWriter(1024); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartObject(); + + switch (i) + { + case 0: + jsonUtf8.WriteStartObject("property name", escape: false); + break; + case 1: + jsonUtf8.WriteStartObject("property name".AsSpan(), escape: false); + break; + case 2: + jsonUtf8.WriteStartObject(Encoding.UTF8.GetBytes("property name"), escape: false); + break; + case 3: + jsonUtf8.WriteStartObject("property name", escape: true); + break; + case 4: + jsonUtf8.WriteStartObject("property name".AsSpan(), escape: true); + break; + case 5: + jsonUtf8.WriteStartObject(Encoding.UTF8.GetBytes("property name"), escape: true); + break; + } + + jsonUtf8.WriteEndObject(); + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + + ArraySegment arraySegment = output.Formatted; + string actualStr = Encoding.UTF8.GetString(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + + Assert.Equal(expectedStr, actualStr); + + output.Dispose(); + } + } + + [Theory] + [InlineData(true, true, 10)] + [InlineData(true, false, 10)] + [InlineData(false, true, 10)] + [InlineData(false, false, 10)] + [InlineData(true, true, 100)] + [InlineData(true, false, 100)] + [InlineData(false, true, 100)] + [InlineData(false, false, 100)] + public void WriteStartEndWithPropertyNameObject(bool formatted, bool skipValidation, int keyLength) + { + var keyChars = new char[keyLength]; + for (int i = 0; i < keyChars.Length; i++) + { + keyChars[i] = '<'; + } + var key = new string(keyChars); + + string expectedStr = GetStartEndWithPropertyObjectExpectedString(key, prettyPrint: formatted, escape: true); + string expectedStrNoEscape = GetStartEndWithPropertyObjectExpectedString(key, prettyPrint: formatted, escape: false); + + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + for (int i = 0; i < 6; i++) + { + var output = new ArrayBufferWriter(1024); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartObject(); + + switch (i) + { + case 0: + jsonUtf8.WriteStartObject(key, escape: false); + break; + case 1: + jsonUtf8.WriteStartObject(key.AsSpan(), escape: false); + break; + case 2: + jsonUtf8.WriteStartObject(Encoding.UTF8.GetBytes(key), escape: false); + break; + case 3: + jsonUtf8.WriteStartObject(key, escape: true); + break; + case 4: + jsonUtf8.WriteStartObject(key.AsSpan(), escape: true); + break; + case 5: + jsonUtf8.WriteStartObject(Encoding.UTF8.GetBytes(key), escape: true); + break; + } + + jsonUtf8.WriteEndObject(); + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + + ArraySegment arraySegment = output.Formatted; + string actualStr = Encoding.UTF8.GetString(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + + if (i < 3) + Assert.Equal(expectedStrNoEscape, actualStr); + else + Assert.Equal(expectedStr, actualStr); + + output.Dispose(); + } + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void WriteArrayWithProperty(bool formatted, bool skipValidation) + { + string expectedStr = GetArrayWithPropertyExpectedString(prettyPrint: formatted); + + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + for (int i = 0; i < 3; i++) + { + var output = new ArrayBufferWriter(1024); + + var jsonUtf8 = new Utf8JsonWriter(output, state); + jsonUtf8.WriteStartObject(); + + switch (i) + { + case 0: + jsonUtf8.WriteStartArray("message", escape: false); + break; + case 1: + jsonUtf8.WriteStartArray("message".AsSpan(), escape: false); + break; + case 2: + jsonUtf8.WriteStartArray(Encoding.UTF8.GetBytes("message"), escape: false); + break; + } + + jsonUtf8.WriteEndArray(); + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + + ArraySegment arraySegment = output.Formatted; + string actualStr = Encoding.UTF8.GetString(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + + Assert.True(expectedStr == actualStr, $"Case: {i}, | Expected: {expectedStr}, | Actual: {actualStr}"); + + output.Dispose(); + } + } + + [Theory] + [InlineData(true, true, true, "message")] + [InlineData(true, false, true, "message")] + [InlineData(false, true, true, "message")] + [InlineData(false, false, true, "message")] + [InlineData(true, true, true, "mess>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(true, false, true, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(false, true, true, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(false, false, true, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(true, true, false, "message")] + [InlineData(true, false, false, "message")] + [InlineData(false, true, false, "message")] + [InlineData(false, false, false, "message")] + [InlineData(true, true, false, "mess>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(true, false, false, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(false, true, false, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(false, false, false, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + public void WriteBooleanValue(bool formatted, bool skipValidation, bool value, string keyString) + { + string expectedStr = GetBooleanExpectedString(prettyPrint: formatted, keyString, value, escape: true); + string expectedStrNoEscape = GetBooleanExpectedString(prettyPrint: formatted, keyString, value, escape: false); + + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + for (int i = 0; i < 6; i++) + { + var output = new ArrayBufferWriter(1024); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartObject(); + + switch (i) + { + case 0: + jsonUtf8.WriteBoolean(keyString, value, escape: false); + break; + case 1: + jsonUtf8.WriteBoolean(keyString.AsSpan(), value, escape: false); + break; + case 2: + jsonUtf8.WriteBoolean(Encoding.UTF8.GetBytes(keyString), value, escape: false); + break; + case 3: + jsonUtf8.WriteBoolean(keyString, value, escape: true); + break; + case 4: + jsonUtf8.WriteBoolean(keyString.AsSpan(), value, escape: true); + break; + case 5: + jsonUtf8.WriteBoolean(Encoding.UTF8.GetBytes(keyString), value, escape: true); + break; + } + + jsonUtf8.WriteStartArray("temp"); + jsonUtf8.WriteBooleanValue(true); + jsonUtf8.WriteBooleanValue(true); + jsonUtf8.WriteBooleanValue(false); + jsonUtf8.WriteBooleanValue(false); + jsonUtf8.WriteEndArray(); + + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + + ArraySegment arraySegment = output.Formatted; + string actualStr = Encoding.UTF8.GetString(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + + if (i < 3) + Assert.True(expectedStrNoEscape == actualStr, $"Case: {i}, | Expected: {expectedStrNoEscape}, | Actual: {actualStr}, | Value: {value}"); + else + Assert.True(expectedStr == actualStr, $"Case: {i}, | Expected: {expectedStr}, | Actual: {actualStr}, | Value: {value}"); + + output.Dispose(); + } + } + + [Theory] + [InlineData(true, true, "message")] + [InlineData(true, false, "message")] + [InlineData(false, true, "message")] + [InlineData(false, false, "message")] + [InlineData(true, true, "mess>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(true, false, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(false, true, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(false, false, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + public void WriteNullValue(bool formatted, bool skipValidation, string keyString) + { + string expectedStr = GetNullExpectedString(prettyPrint: formatted, keyString, escape: true); + string expectedStrNoEscape = GetNullExpectedString(prettyPrint: formatted, keyString, escape: false); + + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + for (int i = 0; i < 6; i++) + { + var output = new ArrayBufferWriter(1024); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartObject(); + + switch (i) + { + case 0: + jsonUtf8.WriteNull(keyString, escape: false); + break; + case 1: + jsonUtf8.WriteNull(keyString.AsSpan(), escape: false); + break; + case 2: + jsonUtf8.WriteNull(Encoding.UTF8.GetBytes(keyString), escape: false); + break; + case 3: + jsonUtf8.WriteNull(keyString, escape: true); + break; + case 4: + jsonUtf8.WriteNull(keyString.AsSpan(), escape: true); + break; + case 5: + jsonUtf8.WriteNull(Encoding.UTF8.GetBytes(keyString), escape: true); + break; + } + + jsonUtf8.WriteStartArray("temp"); + jsonUtf8.WriteNullValue(); + jsonUtf8.WriteNullValue(); + jsonUtf8.WriteEndArray(); + + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + + ArraySegment arraySegment = output.Formatted; + string actualStr = Encoding.UTF8.GetString(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + + if (i < 3) + Assert.True(expectedStrNoEscape == actualStr, $"Case: {i}, | Expected: {expectedStrNoEscape}, | Actual: {actualStr}"); + else + Assert.True(expectedStr == actualStr, $"Case: {i}, | Expected: {expectedStr}, | Actual: {actualStr}"); + + output.Dispose(); + } + } + + [Theory] + [InlineData(true, true, 0)] + [InlineData(true, false, 0)] + [InlineData(false, true, 0)] + [InlineData(false, false, 0)] + [InlineData(true, true, -1)] + [InlineData(true, false, -1)] + [InlineData(false, true, -1)] + [InlineData(false, false, -1)] + [InlineData(true, true, 1)] + [InlineData(true, false, 1)] + [InlineData(false, true, 1)] + [InlineData(false, false, 1)] + [InlineData(true, true, int.MaxValue)] + [InlineData(true, false, int.MaxValue)] + [InlineData(false, true, int.MaxValue)] + [InlineData(false, false, int.MaxValue)] + [InlineData(true, true, int.MinValue)] + [InlineData(true, false, int.MinValue)] + [InlineData(false, true, int.MinValue)] + [InlineData(false, false, int.MinValue)] + [InlineData(true, true, 12345)] + [InlineData(true, false, 12345)] + [InlineData(false, true, 12345)] + [InlineData(false, false, 12345)] + public void WriteIntegerValue(bool formatted, bool skipValidation, int value) + { + string expectedStr = GetIntegerExpectedString(prettyPrint: formatted, value); + + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + for (int i = 0; i < 3; i++) + { + var output = new ArrayBufferWriter(1024); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartObject(); + + switch (i) + { + case 0: + jsonUtf8.WriteNumber("message", value, escape: false); + break; + case 1: + jsonUtf8.WriteNumber("message".AsSpan(), value, escape: false); + break; + case 2: + jsonUtf8.WriteNumber(Encoding.UTF8.GetBytes("message"), value, escape: false); + break; + } + + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + + ArraySegment arraySegment = output.Formatted; + string actualStr = Encoding.UTF8.GetString(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + + Assert.True(expectedStr == actualStr, $"Case: {i}, | Expected: {expectedStr}, | Actual: {actualStr}, | Value: {value}"); + + output.Dispose(); + } + } + + [Theory] + [InlineData(true, true, "message")] + [InlineData(true, false, "message")] + [InlineData(false, true, "message")] + [InlineData(false, false, "message")] + [InlineData(true, true, "mess>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(true, false, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(false, true, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(false, false, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + public void WriteNumbers(bool formatted, bool skipValidation, string keyString) + { + var random = new Random(42); + const int numberOfItems = 1_000; + + var ints = new int[numberOfItems]; + ints[0] = 0; + ints[1] = int.MaxValue; + ints[2] = int.MinValue; + ints[3] = 12345; + ints[4] = -12345; + for (int i = 5; i < numberOfItems; i++) + { + ints[i] = random.Next(int.MinValue, int.MaxValue); + } + + var uints = new uint[numberOfItems]; + uints[0] = uint.MaxValue; + uints[1] = uint.MinValue; + uints[2] = 3294967295; + for (int i = 3; i < numberOfItems; i++) + { + uint thirtyBits = (uint)random.Next(1 << 30); + uint twoBits = (uint)random.Next(1 << 2); + uint fullRange = (thirtyBits << 2) | twoBits; + uints[i] = fullRange; + } + + var longs = new long[numberOfItems]; + longs[0] = 0; + longs[1] = long.MaxValue; + longs[2] = long.MinValue; + longs[3] = 12345678901; + longs[4] = -12345678901; + for (int i = 5; i < numberOfItems; i++) + { + long value = random.Next(int.MinValue, int.MaxValue); + value += value < 0 ? int.MinValue : int.MaxValue; + longs[i] = value; + } + + var ulongs = new ulong[numberOfItems]; + ulongs[0] = ulong.MaxValue; + ulongs[1] = ulong.MinValue; + ulongs[2] = 10446744073709551615; + for (int i = 3; i < numberOfItems; i++) + { + + } + + var doubles = new double[numberOfItems * 2]; + doubles[0] = 0.00; + doubles[1] = double.MaxValue; + doubles[2] = double.MinValue; + doubles[3] = 12.345e1; + doubles[4] = -123.45e1; + for (int i = 5; i < numberOfItems; i++) + { + var value = random.NextDouble(); + if (value < 0.5) + { + doubles[i] = random.NextDouble() * double.MinValue; + } + else + { + doubles[i] = random.NextDouble() * double.MaxValue; + } + } + + for (int i = numberOfItems; i < numberOfItems * 2; i++) + { + var value = random.NextDouble(); + if (value < 0.5) + { + doubles[i] = random.NextDouble() * -1_000_000; + } + else + { + doubles[i] = random.NextDouble() * 1_000_000; + } + } + + var floats = new float[numberOfItems]; + floats[0] = 0.00f; + floats[1] = float.MaxValue; + floats[2] = float.MinValue; + floats[3] = 12.345e1f; + floats[4] = -123.45e1f; + for (int i = 5; i < numberOfItems; i++) + { + double mantissa = (random.NextDouble() * 2.0) - 1.0; + double exponent = Math.Pow(2.0, random.Next(-126, 128)); + floats[i] = (float)(mantissa * exponent); + } + + var decimals = new decimal[numberOfItems * 2]; + decimals[0] = (decimal)0.00; + decimals[1] = decimal.MaxValue; + decimals[2] = decimal.MinValue; + decimals[3] = (decimal)12.345e1; + decimals[4] = (decimal)-123.45e1; + for (int i = 5; i < numberOfItems; i++) + { + var value = random.NextDouble(); + if (value < 0.5) + { + decimals[i] = (decimal)(random.NextDouble() * -78E14); + } + else + { + decimals[i] = (decimal)(random.NextDouble() * 78E14); + } + } + + for (int i = numberOfItems; i < numberOfItems * 2; i++) + { + var value = random.NextDouble(); + if (value < 0.5) + { + decimals[i] = (decimal)(random.NextDouble() * -1_000_000); + } + else + { + decimals[i] = (decimal)(random.NextDouble() * 1_000_000); + } + } + + string expectedStr = GetNumbersExpectedString(prettyPrint: formatted, keyString, ints, uints, longs, ulongs, floats, doubles, decimals, escape: false); + string expectedStrNoEscape = GetNumbersExpectedString(prettyPrint: formatted, keyString, ints, uints, longs, ulongs, floats, doubles, decimals, escape: false); + + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + for (int j = 0; j < 6; j++) + { + var output = new ArrayBufferWriter(1024); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + ReadOnlySpan keyUtf16 = keyString; + ReadOnlySpan keyUtf8 = Encoding.UTF8.GetBytes(keyString); + + jsonUtf8.WriteStartObject(); + + switch (j) + { + case 0: + for (int i = 0; i < floats.Length; i++) + jsonUtf8.WriteNumber(keyString, floats[i], escape: false); + for (int i = 0; i < ints.Length; i++) + jsonUtf8.WriteNumber(keyString, ints[i], escape: false); + for (int i = 0; i < uints.Length; i++) + jsonUtf8.WriteNumber(keyString, uints[i], escape: false); + for (int i = 0; i < doubles.Length; i++) + jsonUtf8.WriteNumber(keyString, doubles[i], escape: false); + for (int i = 0; i < longs.Length; i++) + jsonUtf8.WriteNumber(keyString, longs[i], escape: false); + for (int i = 0; i < ulongs.Length; i++) + jsonUtf8.WriteNumber(keyString, ulongs[i], escape: false); + for (int i = 0; i < decimals.Length; i++) + jsonUtf8.WriteNumber(keyString, decimals[i], escape: false); + jsonUtf8.WriteStartArray(keyString, escape: false); + break; + case 1: + for (int i = 0; i < floats.Length; i++) + jsonUtf8.WriteNumber(keyUtf16, floats[i], escape: false); + for (int i = 0; i < ints.Length; i++) + jsonUtf8.WriteNumber(keyUtf16, ints[i], escape: false); + for (int i = 0; i < uints.Length; i++) + jsonUtf8.WriteNumber(keyUtf16, uints[i], escape: false); + for (int i = 0; i < doubles.Length; i++) + jsonUtf8.WriteNumber(keyUtf16, doubles[i], escape: false); + for (int i = 0; i < longs.Length; i++) + jsonUtf8.WriteNumber(keyUtf16, longs[i], escape: false); + for (int i = 0; i < ulongs.Length; i++) + jsonUtf8.WriteNumber(keyUtf16, ulongs[i], escape: false); + for (int i = 0; i < decimals.Length; i++) + jsonUtf8.WriteNumber(keyUtf16, decimals[i], escape: false); + jsonUtf8.WriteStartArray(keyUtf16, escape: false); + break; + case 2: + for (int i = 0; i < floats.Length; i++) + jsonUtf8.WriteNumber(keyUtf8, floats[i], escape: false); + for (int i = 0; i < ints.Length; i++) + jsonUtf8.WriteNumber(keyUtf8, ints[i], escape: false); + for (int i = 0; i < uints.Length; i++) + jsonUtf8.WriteNumber(keyUtf8, uints[i], escape: false); + for (int i = 0; i < doubles.Length; i++) + jsonUtf8.WriteNumber(keyUtf8, doubles[i], escape: false); + for (int i = 0; i < longs.Length; i++) + jsonUtf8.WriteNumber(keyUtf8, longs[i], escape: false); + for (int i = 0; i < ulongs.Length; i++) + jsonUtf8.WriteNumber(keyUtf8, ulongs[i], escape: false); + for (int i = 0; i < decimals.Length; i++) + jsonUtf8.WriteNumber(keyUtf8, decimals[i], escape: false); + jsonUtf8.WriteStartArray(keyUtf8, escape: false); + break; + case 3: + for (int i = 0; i < floats.Length; i++) + jsonUtf8.WriteNumber(keyString, floats[i], escape: true); + for (int i = 0; i < ints.Length; i++) + jsonUtf8.WriteNumber(keyString, ints[i], escape: true); + for (int i = 0; i < uints.Length; i++) + jsonUtf8.WriteNumber(keyString, uints[i], escape: true); + for (int i = 0; i < doubles.Length; i++) + jsonUtf8.WriteNumber(keyString, doubles[i], escape: true); + for (int i = 0; i < longs.Length; i++) + jsonUtf8.WriteNumber(keyString, longs[i], escape: true); + for (int i = 0; i < ulongs.Length; i++) + jsonUtf8.WriteNumber(keyString, ulongs[i], escape: true); + for (int i = 0; i < decimals.Length; i++) + jsonUtf8.WriteNumber(keyString, decimals[i], escape: true); + jsonUtf8.WriteStartArray(keyString, escape: true); + break; + case 4: + for (int i = 0; i < floats.Length; i++) + jsonUtf8.WriteNumber(keyUtf16, floats[i], escape: true); + for (int i = 0; i < ints.Length; i++) + jsonUtf8.WriteNumber(keyUtf16, ints[i], escape: true); + for (int i = 0; i < uints.Length; i++) + jsonUtf8.WriteNumber(keyUtf16, uints[i], escape: true); + for (int i = 0; i < doubles.Length; i++) + jsonUtf8.WriteNumber(keyUtf16, doubles[i], escape: true); + for (int i = 0; i < longs.Length; i++) + jsonUtf8.WriteNumber(keyUtf16, longs[i], escape: true); + for (int i = 0; i < ulongs.Length; i++) + jsonUtf8.WriteNumber(keyUtf16, ulongs[i], escape: true); + for (int i = 0; i < decimals.Length; i++) + jsonUtf8.WriteNumber(keyUtf16, decimals[i], escape: true); + jsonUtf8.WriteStartArray(keyUtf16, escape: true); + break; + case 5: + for (int i = 0; i < floats.Length; i++) + jsonUtf8.WriteNumber(keyUtf8, floats[i], escape: true); + for (int i = 0; i < ints.Length; i++) + jsonUtf8.WriteNumber(keyUtf8, ints[i], escape: true); + for (int i = 0; i < uints.Length; i++) + jsonUtf8.WriteNumber(keyUtf8, uints[i], escape: true); + for (int i = 0; i < doubles.Length; i++) + jsonUtf8.WriteNumber(keyUtf8, doubles[i], escape: true); + for (int i = 0; i < longs.Length; i++) + jsonUtf8.WriteNumber(keyUtf8, longs[i], escape: true); + for (int i = 0; i < ulongs.Length; i++) + jsonUtf8.WriteNumber(keyUtf8, ulongs[i], escape: true); + for (int i = 0; i < decimals.Length; i++) + jsonUtf8.WriteNumber(keyUtf8, decimals[i], escape: true); + jsonUtf8.WriteStartArray(keyUtf8, escape: true); + break; + } + + jsonUtf8.WriteNumberValue(floats[0]); + jsonUtf8.WriteNumberValue(ints[0]); + jsonUtf8.WriteNumberValue(uints[0]); + jsonUtf8.WriteNumberValue(doubles[0]); + jsonUtf8.WriteNumberValue(longs[0]); + jsonUtf8.WriteNumberValue(ulongs[0]); + jsonUtf8.WriteNumberValue(decimals[0]); + jsonUtf8.WriteEndArray(); + + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + + ArraySegment arraySegment = output.Formatted; + string actualStr = Encoding.UTF8.GetString(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + + // TODO: The output doesn't match what JSON.NET does (different rounding/e-notation). + //if (j < 3) + // Assert.Equal(expectedStrNoEscape, actualStr); + //else + // Assert.Equal(expectedStr, actualStr); + + output.Dispose(); + } + } + + [Theory] + [InlineData(true, true, "message")] + [InlineData(true, false, "message")] + [InlineData(false, true, "message")] + [InlineData(false, false, "message")] + [InlineData(true, true, "mess>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(true, false, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(false, true, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(false, false, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + public void WriteGuidsValue(bool formatted, bool skipValidation, string keyString) + { + const int numberOfItems = 1_000; + + var guids = new Guid[numberOfItems]; + for (int i = 0; i < numberOfItems; i++) + guids[i] = Guid.NewGuid(); + + string expectedStr = GetGuidsExpectedString(prettyPrint: formatted, keyString, guids, escape: true); + string expectedStrNoEscape = GetGuidsExpectedString(prettyPrint: formatted, keyString, guids, escape: false); + + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + ReadOnlySpan keyUtf16 = keyString; + ReadOnlySpan keyUtf8 = Encoding.UTF8.GetBytes(keyString); + + for (int i = 0; i < 6; i++) + { + var output = new ArrayBufferWriter(1024); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartObject(); + + switch (i) + { + case 0: + for (int j = 0; j < numberOfItems; j++) + jsonUtf8.WriteString(keyString, guids[j], escape: false); + jsonUtf8.WriteStartArray(keyString, escape: false); + break; + case 1: + for (int j = 0; j < numberOfItems; j++) + jsonUtf8.WriteString(keyUtf16, guids[j], escape: false); + jsonUtf8.WriteStartArray(keyUtf16, escape: false); + break; + case 2: + for (int j = 0; j < numberOfItems; j++) + jsonUtf8.WriteString(keyUtf8, guids[j], escape: false); + jsonUtf8.WriteStartArray(keyUtf8, escape: false); + break; + case 3: + for (int j = 0; j < numberOfItems; j++) + jsonUtf8.WriteString(keyString, guids[j], escape: true); + jsonUtf8.WriteStartArray(keyString, escape: true); + break; + case 4: + for (int j = 0; j < numberOfItems; j++) + jsonUtf8.WriteString(keyUtf16, guids[j], escape: true); + jsonUtf8.WriteStartArray(keyUtf16, escape: true); + break; + case 5: + for (int j = 0; j < numberOfItems; j++) + jsonUtf8.WriteString(keyUtf8, guids[j], escape: true); + jsonUtf8.WriteStartArray(keyUtf8, escape: true); + break; + } + + jsonUtf8.WriteStringValue(guids[0]); + jsonUtf8.WriteStringValue(guids[1]); + jsonUtf8.WriteEndArray(); + + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + + ArraySegment arraySegment = output.Formatted; + string actualStr = Encoding.UTF8.GetString(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + + if (i < 3) + Assert.Equal(expectedStrNoEscape, actualStr); + else + Assert.Equal(expectedStr, actualStr); + + output.Dispose(); + } + } + + [Theory] + [InlineData(true, true, "message")] + [InlineData(true, false, "message")] + [InlineData(false, true, "message")] + [InlineData(false, false, "message")] + [InlineData(true, true, "mess>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(true, false, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(false, true, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(false, false, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + public void WriteDateTimesValue(bool formatted, bool skipValidation, string keyString) + { + var random = new Random(42); + const int numberOfItems = 1_000; + + var start = new DateTime(1995, 1, 1); + int range = (DateTime.Today - start).Days; + + var dates = new DateTime[numberOfItems]; + for (int i = 0; i < numberOfItems; i++) + dates[i] = start.AddDays(random.Next(range)); + + string expectedStr = GetDatesExpectedString(prettyPrint: formatted, keyString, dates, escape: true); + string expectedStrNoEscape = GetDatesExpectedString(prettyPrint: formatted, keyString, dates, escape: false); + + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + ReadOnlySpan keyUtf16 = keyString; + ReadOnlySpan keyUtf8 = Encoding.UTF8.GetBytes(keyString); + + for (int i = 0; i < 6; i++) + { + var output = new ArrayBufferWriter(1024); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartObject(); + + switch (i) + { + case 0: + for (int j = 0; j < numberOfItems; j++) + jsonUtf8.WriteString(keyString, dates[j], escape: false); + jsonUtf8.WriteStartArray(keyString, escape: false); + break; + case 1: + for (int j = 0; j < numberOfItems; j++) + jsonUtf8.WriteString(keyUtf16, dates[j], escape: false); + jsonUtf8.WriteStartArray(keyUtf16, escape: false); + break; + case 2: + for (int j = 0; j < numberOfItems; j++) + jsonUtf8.WriteString(keyUtf8, dates[j], escape: false); + jsonUtf8.WriteStartArray(keyUtf8, escape: false); + break; + case 3: + for (int j = 0; j < numberOfItems; j++) + jsonUtf8.WriteString(keyString, dates[j], escape: true); + jsonUtf8.WriteStartArray(keyString, escape: true); + break; + case 4: + for (int j = 0; j < numberOfItems; j++) + jsonUtf8.WriteString(keyUtf16, dates[j], escape: true); + jsonUtf8.WriteStartArray(keyUtf16, escape: true); + break; + case 5: + for (int j = 0; j < numberOfItems; j++) + jsonUtf8.WriteString(keyUtf8, dates[j], escape: true); + jsonUtf8.WriteStartArray(keyUtf8, escape: true); + break; + } + + jsonUtf8.WriteStringValue(dates[0]); + jsonUtf8.WriteStringValue(dates[1]); + jsonUtf8.WriteEndArray(); + + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + + ArraySegment arraySegment = output.Formatted; + string actualStr = Encoding.UTF8.GetString(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + + if (i < 3) + Assert.Equal(expectedStrNoEscape, actualStr); + else + Assert.Equal(expectedStr, actualStr); + + output.Dispose(); + } + } + + [Theory] + [InlineData(true, true, "message")] + [InlineData(true, false, "message")] + [InlineData(false, true, "message")] + [InlineData(false, false, "message")] + [InlineData(true, true, "mess>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(true, false, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(false, true, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(false, false, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + public void WriteDateTimeOffsetsValue(bool formatted, bool skipValidation, string keyString) + { + var random = new Random(42); + const int numberOfItems = 1_000; + + var start = new DateTime(1995, 1, 1); + int range = (DateTime.Today - start).Days; + + var dates = new DateTimeOffset[numberOfItems]; + for (int i = 0; i < numberOfItems; i++) + dates[i] = new DateTimeOffset(start.AddDays(random.Next(range))); + + string expectedStr = GetDatesExpectedString(prettyPrint: formatted, keyString, dates, escape: true); + string expectedStrNoEscape = GetDatesExpectedString(prettyPrint: formatted, keyString, dates, escape: false); + + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + ReadOnlySpan keyUtf16 = keyString; + ReadOnlySpan keyUtf8 = Encoding.UTF8.GetBytes(keyString); + + for (int i = 0; i < 6; i++) + { + var output = new ArrayBufferWriter(1024); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartObject(); + + switch (i) + { + case 0: + for (int j = 0; j < numberOfItems; j++) + jsonUtf8.WriteString(keyString, dates[j], escape: false); + jsonUtf8.WriteStartArray(keyString, escape: false); + break; + case 1: + for (int j = 0; j < numberOfItems; j++) + jsonUtf8.WriteString(keyUtf16, dates[j], escape: false); + jsonUtf8.WriteStartArray(keyUtf16, escape: false); + break; + case 2: + for (int j = 0; j < numberOfItems; j++) + jsonUtf8.WriteString(keyUtf8, dates[j], escape: false); + jsonUtf8.WriteStartArray(keyUtf8, escape: false); + break; + case 3: + for (int j = 0; j < numberOfItems; j++) + jsonUtf8.WriteString(keyString, dates[j], escape: true); + jsonUtf8.WriteStartArray(keyString, escape: true); + break; + case 4: + for (int j = 0; j < numberOfItems; j++) + jsonUtf8.WriteString(keyUtf16, dates[j], escape: true); + jsonUtf8.WriteStartArray(keyUtf16, escape: true); + break; + case 5: + for (int j = 0; j < numberOfItems; j++) + jsonUtf8.WriteString(keyUtf8, dates[j], escape: true); + jsonUtf8.WriteStartArray(keyUtf8, escape: true); + break; + } + + jsonUtf8.WriteStringValue(dates[0]); + jsonUtf8.WriteStringValue(dates[1]); + jsonUtf8.WriteEndArray(); + + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + + ArraySegment arraySegment = output.Formatted; + string actualStr = Encoding.UTF8.GetString(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + + if (i < 3) + Assert.Equal(expectedStrNoEscape, actualStr); + else + Assert.Equal(expectedStr, actualStr); + + output.Dispose(); + } + } + + [Theory] + [OuterLoop] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void WriteLargeKeyOrValue(bool formatted, bool skipValidation) + { + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + Span key = new byte[1_000_000_001]; + key.Fill((byte)'a'); + Span value = new byte[1_000_000_001]; + value.Fill((byte)'b'); + + var output = new ArrayBufferWriter(1024); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + try + { + jsonUtf8.WriteStartObject(); + jsonUtf8.WriteString(key, DateTime.Now, escape: false); + Assert.True(false, $"Expected ArgumentException for data too large wasn't thrown. KeyLength: {key.Length}"); + } + catch (ArgumentException) { } + + try + { + jsonUtf8.WriteStartArray(); + jsonUtf8.WriteStringValue(value, escape: false); + Assert.True(false, $"Expected ArgumentException for data too large wasn't thrown. ValueLength: {value.Length}"); + } + catch (ArgumentException) { } + + output.Dispose(); + } + + [Theory] + [OuterLoop] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void WriteLargeKeyValue(bool formatted, bool skipValidation) + { + var state = new JsonWriterState(options: new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }); + + Span key = new byte[1_000_000_001]; + key.Fill((byte)'a'); + Span value = new byte[1_000_000_001]; + value.Fill((byte)'b'); + + WriteTooLargeHelper(state, key, value); + WriteTooLargeHelper(state, key.Slice(0, 1_000_000_000), value); + WriteTooLargeHelper(state, key, value.Slice(0, 1_000_000_000)); + WriteTooLargeHelper(state, key.Slice(0, 10_000_000 / 3), value.Slice(0, 10_000_000 / 3), noThrow: true); + } + + private static void WriteTooLargeHelper(JsonWriterState state, ReadOnlySpan key, ReadOnlySpan value, bool noThrow = false) + { + var output = new ArrayBufferWriter(1024); + var jsonUtf8 = new Utf8JsonWriter(output, state); + + jsonUtf8.WriteStartObject(); + + try + { + jsonUtf8.WriteString(key, value, escape: false); + + if (!noThrow) + { + Assert.True(false, $"Expected ArgumentException for data too large wasn't thrown. KeyLength: {key.Length} | ValueLength: {value.Length}"); + } + } + catch (ArgumentException) + { + if (noThrow) + { + Assert.True(false, $"Expected writing large key/value to succeed. KeyLength: {key.Length} | ValueLength: {value.Length}"); + } + } + + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + + output.Dispose(); + } + + private static void WriterDidNotThrow(bool skipValidation) + { + if (skipValidation) + Assert.True(true, "Did not expect InvalidOperationException to be thrown since validation was skipped."); + else + Assert.True(false, "Expected InvalidOperationException to be thrown when validation is enabled."); + } + + private static void WriterDidNotThrow(bool skipValidation, string message) + { + if (skipValidation) + Assert.True(true, message); + else + Assert.True(false, message); + } + + private static void AssertWriterThrow(bool noThrow) + { + if (noThrow) + Assert.True(true, "Did not expect InvalidOperationException to be thrown since input was valid (or suppressEscaping was true)."); + else + Assert.True(false, "Expected InvalidOperationException to be thrown when user passes invalid UTF-8."); + } + + private static string GetHelloWorldExpectedString(bool prettyPrint) + { + MemoryStream ms = new MemoryStream(); + TextWriter streamWriter = new StreamWriter(ms, new UTF8Encoding(false), 1024, true); + + var json = new JsonTextWriter(streamWriter) + { + Formatting = prettyPrint ? Formatting.Indented : Formatting.None + }; + + json.WriteStartObject(); + json.WritePropertyName("message"); + json.WriteValue("Hello, World!"); + json.WriteEnd(); + + json.Flush(); + + return Encoding.UTF8.GetString(ms.ToArray()); + } + + private static string GetCommentExpectedString(bool prettyPrint, string comment) + { + MemoryStream ms = new MemoryStream(); + TextWriter streamWriter = new StreamWriter(ms, new UTF8Encoding(false), 1024, true); + + var json = new JsonTextWriter(streamWriter) + { + Formatting = prettyPrint ? Formatting.Indented : Formatting.None, + StringEscapeHandling = StringEscapeHandling.EscapeHtml, + }; + + json.WriteStartArray(); + for (int j = 0; j < 10; j++) + json.WriteComment(comment); + json.WriteValue(comment); + json.WriteComment(comment); + json.WriteEnd(); + + json.Flush(); + + return Encoding.UTF8.GetString(ms.ToArray()); + } + + private static string GetStringsExpectedString(bool prettyPrint, string value) + { + MemoryStream ms = new MemoryStream(); + TextWriter streamWriter = new StreamWriter(ms, new UTF8Encoding(false), 1024, true); + + var json = new JsonTextWriter(streamWriter) + { + Formatting = prettyPrint ? Formatting.Indented : Formatting.None + }; + + json.WriteStartArray(); + for (int j = 0; j < 10; j++) + json.WriteValue(value); + json.WriteEnd(); + + json.Flush(); + + return Encoding.UTF8.GetString(ms.ToArray()); + } + + private static string GetEscapedExpectedString(bool prettyPrint, string propertyName, string value, StringEscapeHandling escaping, bool escape = true) + { + MemoryStream ms = new MemoryStream(); + TextWriter streamWriter = new StreamWriter(ms, new UTF8Encoding(false), 1024, true); + + var json = new JsonTextWriter(streamWriter) + { + Formatting = prettyPrint ? Formatting.Indented : Formatting.None, + StringEscapeHandling = escaping + }; + + json.WriteStartObject(); + json.WritePropertyName(propertyName, escape); + json.WriteValue(value); + json.WriteEnd(); + + json.Flush(); + + return Encoding.UTF8.GetString(ms.ToArray()); + } + + private static string GetCustomExpectedString(bool prettyPrint) + { + MemoryStream ms = new MemoryStream(); + TextWriter streamWriter = new StreamWriter(ms, new UTF8Encoding(false), 1024, true); + + var json = new JsonTextWriter(streamWriter) + { + Formatting = prettyPrint ? Formatting.Indented : Formatting.None + }; + + json.WriteStartObject(); + for (int i = 0; i < 1_000; i++) + { + json.WritePropertyName("message"); + json.WriteValue("Hello, World!"); + } + json.WriteEnd(); + + json.Flush(); + + return Encoding.UTF8.GetString(ms.ToArray()); + } + + private static string GetStartEndExpectedString(bool prettyPrint) + { + MemoryStream ms = new MemoryStream(); + TextWriter streamWriter = new StreamWriter(ms, new UTF8Encoding(false), 1024, true); + + var json = new JsonTextWriter(streamWriter) + { + Formatting = prettyPrint ? Formatting.Indented : Formatting.None + }; + + json.WriteStartArray(); + json.WriteStartObject(); + json.WriteEnd(); + json.WriteEnd(); + + json.Flush(); + + return Encoding.UTF8.GetString(ms.ToArray()); + } + + private static string GetStartEndWithPropertyArrayExpectedString(bool prettyPrint) + { + MemoryStream ms = new MemoryStream(); + TextWriter streamWriter = new StreamWriter(ms, new UTF8Encoding(false), 1024, true); + + var json = new JsonTextWriter(streamWriter) + { + Formatting = prettyPrint ? Formatting.Indented : Formatting.None + }; + + json.WriteStartObject(); + json.WritePropertyName("property name"); + json.WriteStartArray(); + json.WriteEnd(); + json.WriteEnd(); + + json.Flush(); + + return Encoding.UTF8.GetString(ms.ToArray()); + } + + private static string GetStartEndWithPropertyArrayExpectedString(string key, bool prettyPrint, bool escape = false) + { + MemoryStream ms = new MemoryStream(); + TextWriter streamWriter = new StreamWriter(ms, new UTF8Encoding(false), 1024, true); + + var json = new JsonTextWriter(streamWriter) + { + Formatting = prettyPrint ? Formatting.Indented : Formatting.None, + StringEscapeHandling = StringEscapeHandling.EscapeHtml + }; + + json.WriteStartObject(); + json.WritePropertyName(key, escape); + json.WriteStartArray(); + json.WriteEnd(); + json.WriteEnd(); + + json.Flush(); + + return Encoding.UTF8.GetString(ms.ToArray()); + } + + private static string GetStartEndWithPropertyObjectExpectedString(bool prettyPrint) + { + MemoryStream ms = new MemoryStream(); + TextWriter streamWriter = new StreamWriter(ms, new UTF8Encoding(false), 1024, true); + + var json = new JsonTextWriter(streamWriter) + { + Formatting = prettyPrint ? Formatting.Indented : Formatting.None + }; + + json.WriteStartObject(); + json.WritePropertyName("property name"); + json.WriteStartObject(); + json.WriteEnd(); + json.WriteEnd(); + + json.Flush(); + + return Encoding.UTF8.GetString(ms.ToArray()); + } + + private static string GetStartEndWithPropertyObjectExpectedString(string key, bool prettyPrint, bool escape = false) + { + MemoryStream ms = new MemoryStream(); + TextWriter streamWriter = new StreamWriter(ms, new UTF8Encoding(false), 1024, true); + + var json = new JsonTextWriter(streamWriter) + { + Formatting = prettyPrint ? Formatting.Indented : Formatting.None, + StringEscapeHandling = StringEscapeHandling.EscapeHtml + }; + + json.WriteStartObject(); + json.WritePropertyName(key, escape); + json.WriteStartObject(); + json.WriteEnd(); + json.WriteEnd(); + + json.Flush(); + + return Encoding.UTF8.GetString(ms.ToArray()); + } + + private static string GetArrayWithPropertyExpectedString(bool prettyPrint) + { + MemoryStream ms = new MemoryStream(); + TextWriter streamWriter = new StreamWriter(ms, new UTF8Encoding(false), 1024, true); + + var json = new JsonTextWriter(streamWriter) + { + Formatting = prettyPrint ? Formatting.Indented : Formatting.None + }; + + json.WriteStartObject(); + json.WritePropertyName("message"); + json.WriteStartArray(); + json.WriteEndArray(); + json.WriteEndObject(); + json.Flush(); + + return Encoding.UTF8.GetString(ms.ToArray()); + } + + private static string GetBooleanExpectedString(bool prettyPrint, string keyString, bool value, bool escape = false) + { + MemoryStream ms = new MemoryStream(); + TextWriter streamWriter = new StreamWriter(ms, new UTF8Encoding(false), 1024, true); + + var json = new JsonTextWriter(streamWriter) + { + Formatting = prettyPrint ? Formatting.Indented : Formatting.None, + StringEscapeHandling = StringEscapeHandling.EscapeHtml, + }; + + json.WriteStartObject(); + json.WritePropertyName(keyString, escape); + json.WriteValue(value); + + json.WritePropertyName("temp"); + json.WriteStartArray(); + json.WriteValue(true); + json.WriteValue(true); + json.WriteValue(false); + json.WriteValue(false); + json.WriteEnd(); + + json.WriteEnd(); + + json.Flush(); + + return Encoding.UTF8.GetString(ms.ToArray()); + } + + private static string GetNullExpectedString(bool prettyPrint, string keyString, bool escape = false) + { + MemoryStream ms = new MemoryStream(); + TextWriter streamWriter = new StreamWriter(ms, new UTF8Encoding(false), 1024, true); + + var json = new JsonTextWriter(streamWriter) + { + Formatting = prettyPrint ? Formatting.Indented : Formatting.None, + StringEscapeHandling = StringEscapeHandling.EscapeHtml, + }; + + json.WriteStartObject(); + json.WritePropertyName(keyString, escape); + json.WriteNull(); + + json.WritePropertyName("temp"); + json.WriteStartArray(); + json.WriteValue((string)null); + json.WriteValue((string)null); + json.WriteEnd(); + + json.WriteEnd(); + + json.Flush(); + + return Encoding.UTF8.GetString(ms.ToArray()); + } + + private static string GetIntegerExpectedString(bool prettyPrint, int value) + { + MemoryStream ms = new MemoryStream(); + TextWriter streamWriter = new StreamWriter(ms, new UTF8Encoding(false), 1024, true); + + var json = new JsonTextWriter(streamWriter) + { + Formatting = prettyPrint ? Formatting.Indented : Formatting.None + }; + + json.WriteStartObject(); + json.WritePropertyName("message"); + json.WriteValue(value); + json.WriteEnd(); + + json.Flush(); + + return Encoding.UTF8.GetString(ms.ToArray()); + } + + private static string GetNumbersExpectedString(bool prettyPrint, string keyString, int[] ints, uint[] uints, long[] longs, ulong[] ulongs, float[] floats, double[] doubles, decimal[] decimals, bool escape = false) + { + MemoryStream ms = new MemoryStream(); + TextWriter streamWriter = new StreamWriter(ms, new UTF8Encoding(false), 1024, true); + + var json = new JsonTextWriter(streamWriter) + { + Formatting = prettyPrint ? Formatting.Indented : Formatting.None + }; + + json.WriteStartObject(); + + for (int i = 0; i < floats.Length; i++) + { + json.WritePropertyName(keyString, escape); + json.WriteValue(floats[i]); + } + for (int i = 0; i < ints.Length; i++) + { + json.WritePropertyName(keyString, escape); + json.WriteValue(ints[i]); + } + for (int i = 0; i < uints.Length; i++) + { + json.WritePropertyName(keyString, escape); + json.WriteValue(uints[i]); + } + for (int i = 0; i < doubles.Length; i++) + { + json.WritePropertyName(keyString, escape); + json.WriteValue(doubles[i]); + } + for (int i = 0; i < longs.Length; i++) + { + json.WritePropertyName(keyString, escape); + json.WriteValue(longs[i]); + } + for (int i = 0; i < ulongs.Length; i++) + { + json.WritePropertyName(keyString, escape); + json.WriteValue(ulongs[i]); + } + for (int i = 0; i < decimals.Length; i++) + { + json.WritePropertyName(keyString, escape); + json.WriteValue(decimals[i]); + } + + json.WritePropertyName(keyString, escape); + json.WriteStartArray(); + json.WriteValue(floats[0]); + json.WriteValue(ints[0]); + json.WriteValue(uints[0]); + json.WriteValue(doubles[0]); + json.WriteValue(longs[0]); + json.WriteValue(ulongs[0]); + json.WriteValue(decimals[0]); + json.WriteEndArray(); + + json.WriteEnd(); + + json.Flush(); + + return Encoding.UTF8.GetString(ms.ToArray()); + } + + private static string GetGuidsExpectedString(bool prettyPrint, string keyString, Guid[] guids, bool escape = false) + { + MemoryStream ms = new MemoryStream(); + TextWriter streamWriter = new StreamWriter(ms, new UTF8Encoding(false), 1024, true); + + var json = new JsonTextWriter(streamWriter) + { + Formatting = prettyPrint ? Formatting.Indented : Formatting.None, + StringEscapeHandling = StringEscapeHandling.EscapeHtml + }; + + json.WriteStartObject(); + + for (int i = 0; i < guids.Length; i++) + { + json.WritePropertyName(keyString, escape); + json.WriteValue(guids[i]); + } + + json.WritePropertyName(keyString, escape); + json.WriteStartArray(); + json.WriteValue(guids[0]); + json.WriteValue(guids[1]); + json.WriteEnd(); + + json.WriteEnd(); + + json.Flush(); + + return Encoding.UTF8.GetString(ms.ToArray()); + } + + private static string GetDatesExpectedString(bool prettyPrint, string keyString, DateTime[] dates, bool escape = false) + { + MemoryStream ms = new MemoryStream(); + TextWriter streamWriter = new StreamWriter(ms, new UTF8Encoding(false), 1024, true); + + var json = new JsonTextWriter(streamWriter) + { + Formatting = prettyPrint ? Formatting.Indented : Formatting.None, + StringEscapeHandling = StringEscapeHandling.EscapeHtml, + DateFormatString = "yyyy-MM-ddTHH:mm:ss.fffffff" + }; + + json.WriteStartObject(); + + for (int i = 0; i < dates.Length; i++) + { + json.WritePropertyName(keyString, escape); + json.WriteValue(dates[i]); + } + + json.WritePropertyName(keyString, escape); + json.WriteStartArray(); + json.WriteValue(dates[0]); + json.WriteValue(dates[1]); + json.WriteEnd(); + + json.WriteEnd(); + + json.Flush(); + + return Encoding.UTF8.GetString(ms.ToArray()); + } + + private static string GetDatesExpectedString(bool prettyPrint, string keyString, DateTimeOffset[] dates, bool escape = false) + { + MemoryStream ms = new MemoryStream(); + TextWriter streamWriter = new StreamWriter(ms, new UTF8Encoding(false), 1024, true); + + var json = new JsonTextWriter(streamWriter) + { + Formatting = prettyPrint ? Formatting.Indented : Formatting.None, + StringEscapeHandling = StringEscapeHandling.EscapeHtml, + DateFormatString = "yyyy-MM-ddTHH:mm:ss.fffffffzzz" + }; + + json.WriteStartObject(); + + for (int i = 0; i < dates.Length; i++) + { + json.WritePropertyName(keyString, escape); + json.WriteValue(dates[i]); + } + + json.WritePropertyName(keyString, escape); + json.WriteStartArray(); + json.WriteValue(dates[0]); + json.WriteValue(dates[1]); + json.WriteEnd(); + + json.WriteEnd(); + + json.Flush(); + + return Encoding.UTF8.GetString(ms.ToArray()); + } + } +}