Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Commit

Permalink
Add JsonEncodedText with Utf8JsonWriter overloads that accept it (#37323
Browse files Browse the repository at this point in the history
)

* Initial impl of JsonEncodedText with tests.

* Auto-generate the reference assembly.

* Update tests and ref assembly to include dummy field.

* Add JsonEncodedText overloads to Utf8JsonWriter with tests.

* Auto-gen the ref

* Address PR feedback.

* Fix typo - dispose the Utf8JsonWriter within serializer.

* Add XML comments to the new APIs and types.

* Alpha order the csproj.
  • Loading branch information
ahsonkhan authored May 2, 2019
1 parent 490fc5a commit 49262a8
Show file tree
Hide file tree
Showing 24 changed files with 1,816 additions and 89 deletions.
36 changes: 36 additions & 0 deletions src/System.Text.Json/ref/System.Text.Json.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,18 @@ public void Reset() { }
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; }
}
}
public readonly partial struct JsonEncodedText : System.IEquatable<System.Text.Json.JsonEncodedText>
{
private readonly object _dummy;
public System.ReadOnlySpan<byte> EncodedUtf8Bytes { get { throw null; } }
public static System.Text.Json.JsonEncodedText Encode(System.ReadOnlySpan<byte> utf8Value) { throw null; }
public static System.Text.Json.JsonEncodedText Encode(System.ReadOnlySpan<char> value) { throw null; }
public static System.Text.Json.JsonEncodedText Encode(string value) { throw null; }
public override bool Equals(object obj) { throw null; }
public bool Equals(System.Text.Json.JsonEncodedText other) { throw null; }
public override int GetHashCode() { throw null; }
public override string ToString() { throw null; }
}
public readonly partial struct JsonProperty
{
private readonly object _dummy;
Expand Down Expand Up @@ -225,6 +237,7 @@ public void Reset(System.IO.Stream utf8Json) { }
public void WriteBoolean(System.ReadOnlySpan<byte> utf8PropertyName, bool value) { }
public void WriteBoolean(System.ReadOnlySpan<char> propertyName, bool value) { }
public void WriteBoolean(string propertyName, bool value) { }
public void WriteBoolean(System.Text.Json.JsonEncodedText propertyName, bool value) { }
public void WriteBooleanValue(bool value) { }
public void WriteCommentValue(System.ReadOnlySpan<byte> utf8Value) { }
public void WriteCommentValue(System.ReadOnlySpan<char> value) { }
Expand All @@ -234,6 +247,7 @@ public void WriteEndObject() { }
public void WriteNull(System.ReadOnlySpan<byte> utf8PropertyName) { }
public void WriteNull(System.ReadOnlySpan<char> propertyName) { }
public void WriteNull(string propertyName) { }
public void WriteNull(System.Text.Json.JsonEncodedText propertyName) { }
public void WriteNullValue() { }
public void WriteNumber(System.ReadOnlySpan<byte> utf8PropertyName, decimal value) { }
public void WriteNumber(System.ReadOnlySpan<byte> utf8PropertyName, double value) { }
Expand Down Expand Up @@ -262,6 +276,15 @@ public void WriteNumber(string propertyName, float value) { }
public void WriteNumber(string propertyName, uint value) { }
[System.CLSCompliantAttribute(false)]
public void WriteNumber(string propertyName, ulong value) { }
public void WriteNumber(System.Text.Json.JsonEncodedText propertyName, decimal value) { }
public void WriteNumber(System.Text.Json.JsonEncodedText propertyName, double value) { }
public void WriteNumber(System.Text.Json.JsonEncodedText propertyName, int value) { }
public void WriteNumber(System.Text.Json.JsonEncodedText propertyName, long value) { }
public void WriteNumber(System.Text.Json.JsonEncodedText propertyName, float value) { }
[System.CLSCompliantAttribute(false)]
public void WriteNumber(System.Text.Json.JsonEncodedText propertyName, uint value) { }
[System.CLSCompliantAttribute(false)]
public void WriteNumber(System.Text.Json.JsonEncodedText propertyName, ulong value) { }
public void WriteNumberValue(decimal value) { }
public void WriteNumberValue(double value) { }
public void WriteNumberValue(int value) { }
Expand All @@ -275,34 +298,47 @@ public void WriteStartArray() { }
public void WriteStartArray(System.ReadOnlySpan<byte> utf8PropertyName) { }
public void WriteStartArray(System.ReadOnlySpan<char> propertyName) { }
public void WriteStartArray(string propertyName) { }
public void WriteStartArray(System.Text.Json.JsonEncodedText propertyName) { }
public void WriteStartObject() { }
public void WriteStartObject(System.ReadOnlySpan<byte> utf8PropertyName) { }
public void WriteStartObject(System.ReadOnlySpan<char> propertyName) { }
public void WriteStartObject(string propertyName) { }
public void WriteStartObject(System.Text.Json.JsonEncodedText propertyName) { }
public void WriteString(System.ReadOnlySpan<byte> utf8PropertyName, System.DateTime value) { }
public void WriteString(System.ReadOnlySpan<byte> utf8PropertyName, System.DateTimeOffset value) { }
public void WriteString(System.ReadOnlySpan<byte> utf8PropertyName, System.Guid value) { }
public void WriteString(System.ReadOnlySpan<byte> utf8PropertyName, System.ReadOnlySpan<byte> utf8Value) { }
public void WriteString(System.ReadOnlySpan<byte> utf8PropertyName, System.ReadOnlySpan<char> value) { }
public void WriteString(System.ReadOnlySpan<byte> utf8PropertyName, string value) { }
public void WriteString(System.ReadOnlySpan<byte> utf8PropertyName, System.Text.Json.JsonEncodedText value) { }
public void WriteString(System.ReadOnlySpan<char> propertyName, System.DateTime value) { }
public void WriteString(System.ReadOnlySpan<char> propertyName, System.DateTimeOffset value) { }
public void WriteString(System.ReadOnlySpan<char> propertyName, System.Guid value) { }
public void WriteString(System.ReadOnlySpan<char> propertyName, System.ReadOnlySpan<byte> utf8Value) { }
public void WriteString(System.ReadOnlySpan<char> propertyName, System.ReadOnlySpan<char> value) { }
public void WriteString(System.ReadOnlySpan<char> propertyName, string value) { }
public void WriteString(System.ReadOnlySpan<char> propertyName, System.Text.Json.JsonEncodedText value) { }
public void WriteString(string propertyName, System.DateTime value) { }
public void WriteString(string propertyName, System.DateTimeOffset value) { }
public void WriteString(string propertyName, System.Guid value) { }
public void WriteString(string propertyName, System.ReadOnlySpan<byte> utf8Value) { }
public void WriteString(string propertyName, System.ReadOnlySpan<char> value) { }
public void WriteString(string propertyName, string value) { }
public void WriteString(string propertyName, System.Text.Json.JsonEncodedText value) { }
public void WriteString(System.Text.Json.JsonEncodedText propertyName, System.DateTime value) { }
public void WriteString(System.Text.Json.JsonEncodedText propertyName, System.DateTimeOffset value) { }
public void WriteString(System.Text.Json.JsonEncodedText propertyName, System.Guid value) { }
public void WriteString(System.Text.Json.JsonEncodedText propertyName, System.ReadOnlySpan<byte> utf8Value) { }
public void WriteString(System.Text.Json.JsonEncodedText propertyName, System.ReadOnlySpan<char> value) { }
public void WriteString(System.Text.Json.JsonEncodedText propertyName, string value) { }
public void WriteString(System.Text.Json.JsonEncodedText propertyName, System.Text.Json.JsonEncodedText value) { }
public void WriteStringValue(System.DateTime value) { }
public void WriteStringValue(System.DateTimeOffset value) { }
public void WriteStringValue(System.Guid value) { }
public void WriteStringValue(System.ReadOnlySpan<byte> utf8Value) { }
public void WriteStringValue(System.ReadOnlySpan<char> value) { }
public void WriteStringValue(string value) { }
public void WriteStringValue(System.Text.Json.JsonEncodedText value) { }
}
}
namespace System.Text.Json.Serialization
Expand Down
15 changes: 9 additions & 6 deletions src/System.Text.Json/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,14 @@
<data name="CannotTranscodeInvalidUtf8" xml:space="preserve">
<value>Cannot transcode invalid UTF-8 JSON text to UTF-16 string.</value>
</data>
<data name="CannotWriteInvalidUTF16" xml:space="preserve">
<value>Cannot write invalid UTF-16 text as JSON. Invalid surrogate value: '{0}'.</value>
<data name="CannotTranscodeInvalidUtf16" xml:space="preserve">
<value>Cannot transcode invalid UTF-16 string to UTF-8 JSON text.</value>
</data>
<data name="CannotWriteInvalidUTF8" xml:space="preserve">
<value>Cannot write invalid UTF-8 text as JSON. Invalid input: '{0}'.</value>
<data name="CannotEncodeInvalidUTF16" xml:space="preserve">
<value>Cannot encode invalid UTF-16 text as JSON. Invalid surrogate value: '{0}'.</value>
</data>
<data name="CannotEncodeInvalidUTF8" xml:space="preserve">
<value>Cannot encode invalid UTF-8 text as JSON. Invalid input: '{0}'.</value>
</data>
<data name="CannotWritePropertyWithinArray" xml:space="preserve">
<value>Cannot write a JSON property within an array or as the first JSON token. Current token type is '{0}'.</value>
Expand Down Expand Up @@ -238,7 +241,7 @@
<value>The maximum configured depth of {0} has been exceeded. Cannot read next JSON object.</value>
</data>
<data name="PropertyNameTooLarge" xml:space="preserve">
<value>The JSON property name of length {0} is too large and not supported by the JSON writer.</value>
<value>The JSON property name of length {0} is too large and not supported.</value>
</data>
<data name="FormatDecimal" xml:space="preserve">
<value>The JSON value is either too large or too small for a Decimal.</value>
Expand Down Expand Up @@ -274,7 +277,7 @@
<value>.NET number values such as positive and negative infinity cannot be written as valid JSON.</value>
</data>
<data name="ValueTooLarge" xml:space="preserve">
<value>The JSON value of length {0} is too large and not supported by the JSON writer.</value>
<value>The JSON value of length {0} is too large and not supported.</value>
</data>
<data name="ZeroDepthAtEnd" xml:space="preserve">
<value>Expected depth to be zero at the end of the JSON payload. There is an open JSON object or array that should be closed.</value>
Expand Down
1 change: 1 addition & 0 deletions src/System.Text.Json/src/System.Text.Json.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<Compile Include="System\Text\Json\BitStack.cs" />
<Compile Include="System\Text\Json\JsonCommentHandling.cs" />
<Compile Include="System\Text\Json\JsonConstants.cs" />
<Compile Include="System\Text\Json\JsonEncodedText.cs" />
<Compile Include="System\Text\Json\JsonHelpers.cs" />
<Compile Include="System\Text\Json\JsonHelpers.Date.cs" />
<Compile Include="System\Text\Json\JsonTokenType.cs" />
Expand Down
203 changes: 203 additions & 0 deletions src/System.Text.Json/src/System/Text/Json/JsonEncodedText.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// 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
{
/// <summary>
/// Provides a way to transform UTF-8 or UTF-16 encoded text into a form that is suitable for JSON.
/// </summary>
/// <remarks>
/// This can be used to cache and store known strings used for writing JSON ahead of time by pre-encoding them up front.
/// </remarks>
public readonly struct JsonEncodedText : IEquatable<JsonEncodedText>
{
private readonly byte[] _utf8Value;
private readonly string _value;

/// <summary>
/// Returns the UTF-8 encoded representation of the pre-encoded JSON text.
/// </summary>
public ReadOnlySpan<byte> EncodedUtf8Bytes => _utf8Value;

private JsonEncodedText(byte[] utf8Value)
{
Debug.Assert(utf8Value != null);

_value = JsonReaderHelper.GetTextFromUtf8(utf8Value);
_utf8Value = utf8Value;
}

/// <summary>
/// Encodes the string text value as a JSON string.
/// </summary>
/// <param name="value">The UTF-16 encoded value to be transformed as JSON encoded text.</param>
/// <exception cref="ArgumentNullException">
/// Thrown if value is null.
/// </exception>
/// <exception cref="ArgumentException">
/// Thrown when the specified value is too large or if it contains invalid UTF-16 characters.
/// </exception>
public static JsonEncodedText Encode(string value)
{
if (value == null)
throw new ArgumentNullException(nameof(value));

return Encode(value.AsSpan());
}

/// <summary>
/// Encodes the UTF-16 text value as a JSON string.
/// </summary>
/// <param name="value">The UTF-16 encoded value to be transformed as JSON encoded text.</param>
/// <exception cref="ArgumentException">
/// Thrown when the specified value is too large or if it contains invalid UTF-16 characters.
/// </exception>
public static JsonEncodedText Encode(ReadOnlySpan<char> value)
{
if (value.Length == 0)
{
return new JsonEncodedText(Array.Empty<byte>());
}

return TranscodeAndEncode(value);
}

private static JsonEncodedText TranscodeAndEncode(ReadOnlySpan<char> value)
{
JsonWriterHelper.ValidateValue(value);

int expectedByteCount = JsonReaderHelper.GetUtf8ByteCount(value);
byte[] utf8Bytes = ArrayPool<byte>.Shared.Rent(expectedByteCount);

JsonEncodedText encodedText;

// Since GetUtf8ByteCount above already throws on invalid input, the transcoding
// to UTF-8 is guaranteed to succeed here. Therefore, there's no need for a try-catch-finally block.
int actualByteCount = JsonReaderHelper.GetUtf8FromText(value, utf8Bytes);
Debug.Assert(expectedByteCount == actualByteCount);

encodedText = EncodeHelper(utf8Bytes.AsSpan(0, actualByteCount));

// On the basis that this is user data, go ahead and clear it.
utf8Bytes.AsSpan(0, expectedByteCount).Clear();
ArrayPool<byte>.Shared.Return(utf8Bytes);

return encodedText;
}

/// <summary>
/// Encodes the UTF-8 text value as a JSON string.
/// </summary>
/// <param name="utf8Value">The UTF-8 encoded value to be transformed as JSON encoded text.</param>
/// <exception cref="ArgumentException">
/// Thrown when the specified value is too large or if it contains invalid UTF-8 bytes.
/// </exception>
public static JsonEncodedText Encode(ReadOnlySpan<byte> utf8Value)
{
if (utf8Value.Length == 0)
{
return new JsonEncodedText(Array.Empty<byte>());
}

JsonWriterHelper.ValidateValue(utf8Value);
return EncodeHelper(utf8Value);
}

private static JsonEncodedText EncodeHelper(ReadOnlySpan<byte> utf8Value)
{
int idx = JsonWriterHelper.NeedsEscaping(utf8Value);

if (idx != -1)
{
return new JsonEncodedText(GetEscapedString(utf8Value, idx));
}
else
{
return new JsonEncodedText(utf8Value.ToArray());
}
}

private static byte[] GetEscapedString(ReadOnlySpan<byte> 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<byte> escapedValue = length <= JsonConstants.StackallocThreshold ?
stackalloc byte[length] :
(valueArray = ArrayPool<byte>.Shared.Rent(length));

JsonWriterHelper.EscapeString(utf8Value, escapedValue, firstEscapeIndexVal, out int written);

byte[] escapedString = escapedValue.Slice(0, written).ToArray();

if (valueArray != null)
{
ArrayPool<byte>.Shared.Return(valueArray);
}

return escapedString;
}

/// <summary>
/// Determines whether this instance and another specified <see cref="JsonEncodedText"/> instance have the same value.
/// </summary>
/// <remarks>
/// Default instances of <see cref="JsonEncodedText"/> are treated as equal.
/// </remarks>
public bool Equals(JsonEncodedText other)
{
if (_value == null)
{
return other._value == null;
}
else
{
return _value.Equals(other._value);
}
}

/// <summary>
/// Determines whether this instance and a specified object, which must also be a <see cref="JsonEncodedText"/> instance, have the same value.
/// </summary>
/// <remarks>
/// If <paramref name="obj"/> is null, the method returns false.
/// </remarks>
public override bool Equals(object obj)
{
if (obj is JsonEncodedText encodedText)
{
return Equals(encodedText);
}
return false;
}

/// <summary>
/// Converts the value of this instance to a <see cref="string"/>.
/// </summary>
/// <remarks>
/// Returns the underlying UTF-16 encoded string.
/// </remarks>
/// <remarks>
/// Returns an empty string on a default instance of <see cref="JsonEncodedText"/>.
/// </remarks>
public override string ToString()
=> _value ?? string.Empty;

/// <summary>
/// Returns the hash code for this <see cref="JsonEncodedText"/>.
/// </summary>
/// <remarks>
/// Returns 0 on a default instance of <see cref="JsonEncodedText"/>.
/// </remarks>
public override int GetHashCode()
=> _value == null ? 0 : _value.GetHashCode();
}
}
Loading

0 comments on commit 49262a8

Please sign in to comment.