Add UTF-8 APIs on TextEncoder and add JavascriptEncoder to JsonEncodedText (dotnet...
authorSteve Harter <steveharter@users.noreply.github.com>
Mon, 15 Jul 2019 16:39:58 +0000 (09:39 -0700)
committerGitHub <noreply@github.com>
Mon, 15 Jul 2019 16:39:58 +0000 (09:39 -0700)
Commit migrated from https://github.com/dotnet/corefx/commit/bbbcac59ac07f344ce4e257ef182a90c429d2b58

32 files changed:
src/libraries/System.Text.Encodings.Web/Directory.Build.props
src/libraries/System.Text.Encodings.Web/ref/System.Text.Encodings.Web.cs
src/libraries/System.Text.Encodings.Web/src/System/Text/Encodings/Web/TextEncoder.cs
src/libraries/System.Text.Json/ref/System.Text.Json.cs
src/libraries/System.Text.Json/ref/System.Text.Json.csproj
src/libraries/System.Text.Json/src/System.Text.Json.csproj
src/libraries/System.Text.Json/src/System/Text/Json/JsonEncodedText.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/JsonWriterHelper.Escaping.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Bytes.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.DateTime.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.DateTimeOffset.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Decimal.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Double.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Float.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.FormattedNumber.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Guid.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Literal.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.SignedNumber.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.String.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.UnsignedNumber.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Helpers.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.String.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs
src/libraries/System.Text.Json/tests/JsonElementWriteTests.cs
src/libraries/System.Text.Json/tests/JsonEncodedTextTests.cs
src/libraries/System.Text.Json/tests/Serialization/Object.ReadTests.cs
src/libraries/System.Text.Json/tests/Serialization/TestClasses.SimpleTestClassWithObjectArrays.cs
src/libraries/System.Text.Json/tests/Serialization/Value.ReadTests.cs
src/libraries/System.Text.Json/tests/Serialization/Value.WriteTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj
src/libraries/System.Text.Json/tests/Utf8JsonWriterTests.cs
src/libraries/pkg/Microsoft.Private.PackageBaseline/packageIndex.json

index e3343ce..b0566c6 100644 (file)
@@ -4,5 +4,6 @@
     <AssemblyVersion>4.0.4.0</AssemblyVersion>
     <StrongNameKeyId>Open</StrongNameKeyId>
     <IsNETCoreApp>true</IsNETCoreApp>
+       <IsUAP>true</IsUAP>
   </PropertyGroup>
 </Project>
\ No newline at end of file
index 45da8a9..74cd392 100644 (file)
@@ -32,9 +32,12 @@ namespace System.Text.Encodings.Web
         public void Encode(System.IO.TextWriter output, string value) { }
         public virtual void Encode(System.IO.TextWriter output, string value, int startIndex, int characterCount) { }
         public virtual string Encode(string value) { throw null; }
+        public virtual System.Buffers.OperationStatus EncodeUtf8(System.ReadOnlySpan<byte> utf8Source, System.Span<byte> utf8Destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) { throw null; }
         [System.CLSCompliantAttribute(false)]
         [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
         public unsafe abstract int FindFirstCharacterToEncode(char* text, int textLength);
+        [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
+        public virtual int FindFirstCharacterToEncodeUtf8(System.ReadOnlySpan<byte> utf8Text) { throw null; }
         [System.CLSCompliantAttribute(false)]
         [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
         public unsafe abstract bool TryEncodeUnicodeScalar(int unicodeScalar, char* buffer, int bufferLength, out int numberOfCharactersWritten);
index b9eddfc..0a3cae9 100644 (file)
@@ -21,6 +21,10 @@ namespace System.Text.Encodings.Web
     /// </remarks>
     public abstract class TextEncoder
     {
+        // Fast cache for Ascii
+        private static readonly byte[] s_noEscape = new byte[] { }; // Should not be Array.Empty<byte> since used as a singleton for comparison.
+        private byte[][] _asciiEscape = new byte[0x80][];
+        
         // The following pragma disables a warning complaining about non-CLS compliant members being abstract, 
         // and wants me to mark the type as non-CLS compliant. 
         // It is true that this type cannot be extended by all CLS compliant languages. 
@@ -111,10 +115,10 @@ namespace System.Text.Encodings.Web
                     else
                     {
                         char[] wholebuffer = new char[bufferSize];
-                        fixed(char* buffer = &wholebuffer[0])
+                        fixed (char* buffer = &wholebuffer[0])
                         {
                             int totalWritten = EncodeIntoBuffer(buffer, bufferSize, valuePointer, value.Length, firstCharacterToEncode);
-                            result = new string(wholebuffer, 0, totalWritten);                            
+                            result = new string(wholebuffer, 0, totalWritten);
                         }
                     }
 
@@ -336,168 +340,178 @@ namespace System.Text.Encodings.Web
         /// <see langword="false"/> if there is no further source data that needs to be encoded.</param>
         /// <returns>An <see cref="OperationStatus"/> describing the result of the encoding operation.</returns>
         /// <remarks>The buffers <paramref name="utf8Source"/> and <paramref name="utf8Destination"/> must not overlap.</remarks>
-        internal unsafe virtual OperationStatus EncodeUtf8(ReadOnlySpan<byte> utf8Source, Span<byte> utf8Destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true)
+        public unsafe virtual OperationStatus EncodeUtf8(ReadOnlySpan<byte> utf8Source, Span<byte> utf8Destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true)
         {
-            // Optimization: Detect how much "doesn't require escaping" data exists at the beginning of the buffer,
-            // and memcpy it directly to the destination.
-
-            int numBytesToCopy = FindFirstCharacterToEncodeUtf8(utf8Source);
-            if (numBytesToCopy < 0)
-            {
-                numBytesToCopy = utf8Source.Length;
-            }
-
-            if (!utf8Source.Slice(0, numBytesToCopy).TryCopyTo(utf8Destination))
-            {
-                // There wasn't enough room in the destination to copy over the entire source buffer.
-                // We'll instead copy over as much as we can and return DestinationTooSmall. We do need to
-                // account for the fact that we don't want to truncate a multi-byte UTF-8 subsequence
-                // mid-sequence (since a subsequent slice and call to EncodeUtf8 would produce invalid
-                // data).
-
-                utf8Source = utf8Source.Slice(0, utf8Destination.Length + 1); // guaranteed not to fail since utf8Source is larger than utf8Destination
-                for (int i = utf8Source.Length - 1; i >= 0; i--)
-                {
-                    if (!UnicodeHelpers.IsUtf8ContinuationByte(in utf8Source[i]))
-                    {
-                        utf8Source.Slice(0, i).CopyTo(utf8Destination);
-                        bytesConsumed = i;
-                        bytesWritten = i;
-                        return OperationStatus.DestinationTooSmall;
-                    }
-                }
-
-                // If we got to this point, either somebody mutated the input buffer out from under us, or
-                // the FindFirstCharacterToEncodeUtf8 method was overridden incorrectly such that it attempted
-                // to skip over ill-formed data. In either case we don't know how to perform a partial memcpy
-                // so we shouldn't do anything at all. We'll return DestinationTooSmall here since the caller
-                // can resolve the issue by increasing the size of the destination buffer so that it's at least
-                // as large as the input buffer, which would skip over this entire code path.
-
-                bytesConsumed = 0;
-                bytesWritten = 0;
-                return OperationStatus.DestinationTooSmall;
-            }
-
-            // If we copied over all of the input data, success!
-
-            if (numBytesToCopy == utf8Source.Length)
-            {
-                bytesConsumed = numBytesToCopy;
-                bytesWritten = numBytesToCopy;
-                return OperationStatus.Done;
-            }
-
-            // There's data that must be encoded. Fall back to the scalar-by-scalar slow path.
-
             int originalUtf8SourceLength = utf8Source.Length;
             int originalUtf8DestinationLength = utf8Destination.Length;
-
-            utf8Source = utf8Source.Slice(numBytesToCopy);
-            utf8Destination = utf8Destination.Slice(numBytesToCopy);
-
+            
             const int TempUtf16CharBufferLength = 24; // arbitrarily chosen, but sufficient for any reasonable implementation
             char* pTempCharBuffer = stackalloc char[TempUtf16CharBufferLength];
 
             const int TempUtf8ByteBufferLength = TempUtf16CharBufferLength * 3 /* max UTF-8 output code units per UTF-16 input code unit */;
             byte* pTempUtf8Buffer = stackalloc byte[TempUtf8ByteBufferLength];
 
+            uint nextScalarValue;
+            int utf8BytesConsumedForScalar = 0;
+            int nonEscapedByteCount = 0;
+            OperationStatus opStatus = OperationStatus.Done;
+
             while (!utf8Source.IsEmpty)
             {
-                OperationStatus opStatus = UnicodeHelpers.DecodeScalarValueFromUtf8(utf8Source, out uint nextScalarValue, out int bytesConsumedThisIteration);
-
-                switch (opStatus)
+                // For performance, read until we require escaping.
+                do
                 {
-                    case OperationStatus.Done:
+                    nextScalarValue = utf8Source[nonEscapedByteCount];
+                    if (UnicodeUtility.IsAsciiCodePoint(nextScalarValue))
+                    {
+                        // Check Ascii cache.
+                        byte[] encodedBytes = GetAsciiEncoding((byte)nextScalarValue);
 
-                        if (WillEncode((int)nextScalarValue))
-                        {
-                            goto default; // source data must be transcoded
-                        }
-                        else
+                        if (ReferenceEquals(encodedBytes, s_noEscape))
                         {
-                            // Source data can be copied as-is. Attempt to memcpy it to the destination buffer.
-
-                            if (utf8Source.Slice(0, bytesConsumedThisIteration).TryCopyTo(utf8Destination))
-                            {
-                                utf8Destination = utf8Destination.Slice(bytesConsumedThisIteration);
-                            }
-                            else
+                            if (++nonEscapedByteCount <= utf8Destination.Length)
                             {
-                                goto ReturnDestinationTooSmall;
+                                // Source data can be copied as-is.
+                                continue;
                             }
-                        }
 
-                        break;
-
-                    case OperationStatus.NeedMoreData:
-
-                        if (isFinalBlock)
-                        {
-                            goto default; // treat this as a normal invalid subsequence
+                            --nonEscapedByteCount;
+                            opStatus = OperationStatus.DestinationTooSmall;
+                            break;
                         }
-                        else
+
+                        if (encodedBytes == null)
                         {
-                            goto ReturnNeedMoreData;
+                            // We need to escape and update the cache, so break out of this loop.
+                            opStatus = OperationStatus.Done;
+                            utf8BytesConsumedForScalar = 1;
+                            break;
                         }
 
-                    default:
+                        // For performance, handle the non-escaped bytes and encoding here instead of breaking out of the loop.
+                        if (nonEscapedByteCount > 0)
+                        {
+                            // We previously verified the destination size.
+                            Debug.Assert(nonEscapedByteCount <= utf8Destination.Length);
 
-                        // This code path is hit for ill-formed input data (where decoding has replaced it with U+FFFD)
-                        // and for well-formed input data that must be escaped.
+                            utf8Source.Slice(0, nonEscapedByteCount).CopyTo(utf8Destination);
+                            utf8Source = utf8Source.Slice(nonEscapedByteCount);
+                            utf8Destination = utf8Destination.Slice(nonEscapedByteCount);
+                            nonEscapedByteCount = 0;
+                        }
 
-                        if (TryEncodeUnicodeScalar((int)nextScalarValue, pTempCharBuffer, TempUtf16CharBufferLength, out int charsWrittenJustNow))
+                        if (!((ReadOnlySpan<byte>)encodedBytes).TryCopyTo(utf8Destination))
                         {
-                            // Now that we have it as UTF-16, transcode it to UTF-8.
-                            // Need to copy it to a temporary buffer first, otherwise GetBytes might throw an exception
-                            // due to lack of output space.
+                            opStatus = OperationStatus.DestinationTooSmall;
+                            break;
+                        }
 
-                            int transcodedByteCountThisIteration = Encoding.UTF8.GetBytes(pTempCharBuffer, charsWrittenJustNow, pTempUtf8Buffer, TempUtf8ByteBufferLength);
-                            ReadOnlySpan<byte> transcodedUtf8BytesThisIteration = new ReadOnlySpan<byte>(pTempUtf8Buffer, transcodedByteCountThisIteration);
+                        utf8Destination = utf8Destination.Slice(encodedBytes.Length);
+                        utf8Source = utf8Source.Slice(1);
+                        continue;
+                    }
 
-                            if (!transcodedUtf8BytesThisIteration.TryCopyTo(utf8Destination))
+                    // Code path for non-Ascii.
+                    opStatus = UnicodeHelpers.DecodeScalarValueFromUtf8(utf8Source.Slice(nonEscapedByteCount), out nextScalarValue, out utf8BytesConsumedForScalar);
+                    if (opStatus == OperationStatus.Done)
+                    {
+                        if (!WillEncode((int)nextScalarValue))
+                        {
+                            nonEscapedByteCount += utf8BytesConsumedForScalar;
+                            if (nonEscapedByteCount <= utf8Destination.Length)
                             {
-                                goto ReturnDestinationTooSmall;
+                                // Source data can be copied as-is.
+                                continue;
                             }
 
-                            utf8Destination = utf8Destination.Slice(transcodedByteCountThisIteration); // advance destination buffer
+                            nonEscapedByteCount -= utf8BytesConsumedForScalar;
+                            opStatus = OperationStatus.DestinationTooSmall;
                         }
-                        else
-                        {
-                            // We really don't expect this to fail. If that happens we'll report an error to our caller.
+                    }
+
+                    // We need to escape.
+                    break;
+                } while (nonEscapedByteCount < utf8Source.Length);
+
+                if (nonEscapedByteCount > 0)
+                {
+                    // We previously verified the destination size.
+                    Debug.Assert(nonEscapedByteCount <= utf8Destination.Length);
+
+                    utf8Source.Slice(0, nonEscapedByteCount).CopyTo(utf8Destination);
+                    utf8Source = utf8Source.Slice(nonEscapedByteCount);
+                    utf8Destination = utf8Destination.Slice(nonEscapedByteCount);
+                    nonEscapedByteCount = 0;
+                }
+
+                if (utf8Source.IsEmpty)
+                {
+                    goto Done;
+                }
+
+                // This code path is hit for ill-formed input data (where decoding has replaced it with U+FFFD)
+                // and for well-formed input data that must be escaped.
 
-                            goto ReturnInvalidData;
+                if (opStatus != OperationStatus.Done) // Optimize happy path.
+                {
+                    if (opStatus == OperationStatus.NeedMoreData)
+                    {
+                        if (!isFinalBlock)
+                        {
+                            bytesConsumed = originalUtf8SourceLength - utf8Source.Length;
+                            bytesWritten = originalUtf8DestinationLength - utf8Destination.Length;
+                            return OperationStatus.NeedMoreData;
                         }
+                    }
+                    else if (opStatus == OperationStatus.DestinationTooSmall)
+                    {
+                        goto ReturnDestinationTooSmall;
+                    }
+                }
+
+                if (TryEncodeUnicodeScalar((int)nextScalarValue, pTempCharBuffer, TempUtf16CharBufferLength, out int charsWrittenJustNow))
+                {
+                    // Now that we have it as UTF-16, transcode it to UTF-8.
+                    // Need to copy it to a temporary buffer first, otherwise GetBytes might throw an exception
+                    // due to lack of output space.
+
+                    int transcodedByteCountThisIteration = Encoding.UTF8.GetBytes(pTempCharBuffer, charsWrittenJustNow, pTempUtf8Buffer, TempUtf8ByteBufferLength);
+                    ReadOnlySpan<byte> transcodedUtf8BytesThisIteration = new ReadOnlySpan<byte>(pTempUtf8Buffer, transcodedByteCountThisIteration);
+
+                    // Update cache for Ascii
+                    if (UnicodeUtility.IsAsciiCodePoint(nextScalarValue))
+                    {
+                        _asciiEscape[nextScalarValue] = transcodedUtf8BytesThisIteration.ToArray();
+                    }
 
-                        break;
+                    if (!transcodedUtf8BytesThisIteration.TryCopyTo(utf8Destination))
+                    {
+                        goto ReturnDestinationTooSmall;
+                    }
+
+                    utf8Destination = utf8Destination.Slice(transcodedByteCountThisIteration);
+                }
+                else
+                {
+                    // We really don't expect this to fail. If that happens we'll report an error to our caller.
+                    bytesConsumed = originalUtf8SourceLength - utf8Source.Length;
+                    bytesWritten = originalUtf8DestinationLength - utf8Destination.Length;
+                    return OperationStatus.InvalidData;
                 }
 
-                utf8Source = utf8Source.Slice(bytesConsumedThisIteration);
+                utf8Source = utf8Source.Slice(utf8BytesConsumedForScalar);
             }
 
+        Done:
             // Input buffer has been fully processed!
-
             bytesConsumed = originalUtf8SourceLength;
             bytesWritten = originalUtf8DestinationLength - utf8Destination.Length;
             return OperationStatus.Done;
 
         ReturnDestinationTooSmall:
-
             bytesConsumed = originalUtf8SourceLength - utf8Source.Length;
             bytesWritten = originalUtf8DestinationLength - utf8Destination.Length;
             return OperationStatus.DestinationTooSmall;
-
-        ReturnNeedMoreData:
-
-            bytesConsumed = originalUtf8SourceLength - utf8Source.Length;
-            bytesWritten = originalUtf8DestinationLength - utf8Destination.Length;
-            return OperationStatus.NeedMoreData;
-
-        ReturnInvalidData:
-
-            bytesConsumed = originalUtf8SourceLength - utf8Source.Length;
-            bytesWritten = originalUtf8DestinationLength - utf8Destination.Length;
-            return OperationStatus.InvalidData;
         }
 
         /// <summary>
@@ -587,7 +601,7 @@ namespace System.Text.Encodings.Web
         /// current encoder instance, or -1 if no data in <paramref name="utf8Text"/> requires escaping.
         /// </returns>
         [EditorBrowsable(EditorBrowsableState.Never)]
-        internal virtual int FindFirstCharacterToEncodeUtf8(ReadOnlySpan<byte> utf8Text)
+        public virtual int FindFirstCharacterToEncodeUtf8(ReadOnlySpan<byte> utf8Text)
         {
             int originalUtf8TextLength = utf8Text.Length;
 
@@ -596,15 +610,34 @@ namespace System.Text.Encodings.Web
             // input sequence. If we consume the entire text without seeing either of these, return -1 to indicate
             // that the text can be copied as-is without escaping.
 
-            while (!utf8Text.IsEmpty)
+            int i = 0;
+            while (i < utf8Text.Length)
             {
-                if (UnicodeHelpers.DecodeScalarValueFromUtf8(utf8Text, out uint nextScalarValue, out int bytesConsumedThisIteration) != OperationStatus.Done
-                   || WillEncode((int)nextScalarValue))
+                byte value = utf8Text[i];
+                if (UnicodeUtility.IsAsciiCodePoint(value))
                 {
-                    return originalUtf8TextLength - utf8Text.Length;
+                    if (!ReferenceEquals(GetAsciiEncoding(value), s_noEscape))
+                    {
+                        return originalUtf8TextLength - utf8Text.Length + i;
+                    }
+
+                    i++;
                 }
+                else
+                {
+                    if (i > 0)
+                    {
+                        utf8Text = utf8Text.Slice(i);
+                    }
+
+                    if (UnicodeHelpers.DecodeScalarValueFromUtf8(utf8Text, out uint nextScalarValue, out int bytesConsumedThisIteration) != OperationStatus.Done
+                      || WillEncode((int)nextScalarValue))
+                    {
+                        return originalUtf8TextLength - utf8Text.Length;
+                    }
 
-                utf8Text = utf8Text.Slice(bytesConsumedThisIteration);
+                    i = bytesConsumedThisIteration;
+                }
             }
 
             return -1; // no input data needs to be escaped
@@ -675,5 +708,21 @@ namespace System.Text.Encodings.Web
                 input++;
             }
         }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private byte[] GetAsciiEncoding(byte value)
+        {
+            byte[] encoding = _asciiEscape[value];
+            if (encoding == null)
+            {
+                if (!WillEncode(value))
+                {
+                    _asciiEscape[value] = s_noEscape;
+                    return s_noEscape;
+                }
+            }
+
+            return encoding;
+        }
     }
 }
index 5b60ca2..ee3663c 100644 (file)
@@ -128,9 +128,9 @@ namespace System.Text.Json
     {
         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 static System.Text.Json.JsonEncodedText Encode(System.ReadOnlySpan<byte> utf8Value, System.Text.Encodings.Web.JavaScriptEncoder encoder = null) { throw null; }
+        public static System.Text.Json.JsonEncodedText Encode(System.ReadOnlySpan<char> value, System.Text.Encodings.Web.JavaScriptEncoder encoder = null) { throw null; }
+        public static System.Text.Json.JsonEncodedText Encode(string value, System.Text.Encodings.Web.JavaScriptEncoder encoder = null) { 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; }
index f0f56f7..e540f9f 100644 (file)
@@ -9,12 +9,14 @@
   <ItemGroup Condition="'$(TargetGroup)' != 'netstandard' AND '$(TargetsNetFx)' != 'true'">
     <ProjectReference Include="..\..\System.Memory\ref\System.Memory.csproj" />
     <ProjectReference Include="..\..\System.Runtime\ref\System.Runtime.csproj" />
+    <ProjectReference Include="..\..\System.Text.Encodings.Web\ref\System.Text.Encodings.Web.csproj" />
   </ItemGroup>
   <ItemGroup Condition="'$(TargetGroup)' == 'netstandard' OR '$(TargetsNetFx)' == 'true'">
     <Reference Include="mscorlib" />
     <Reference Include="netstandard" />
     <Reference Include="System.Memory" />
     <Reference Include="System.Threading.Tasks.Extensions" />
+    <ProjectReference Include="..\..\System.Text.Encodings.Web\ref\System.Text.Encodings.Web.csproj" />
     <ProjectReference Include="..\..\Microsoft.Bcl.AsyncInterfaces\ref\Microsoft.Bcl.AsyncInterfaces.csproj" />
   </ItemGroup>
 </Project>
\ No newline at end of file
index cd7c046..b874c93 100644 (file)
     <Reference Include="System.Memory" />
     <Reference Include="System.Numerics.Vectors" />
     <Reference Include="System.Runtime.CompilerServices.Unsafe" />
+    <Reference Include="System.Text.Encodings.Web" />
     <Reference Include="System.Threading.Tasks" />
     <Reference Include="System.Threading.Tasks.Extensions" />
   </ItemGroup>
index 5f2325e..0fb5c89 100644 (file)
@@ -4,6 +4,7 @@
 
 using System.Buffers;
 using System.Diagnostics;
+using System.Text.Encodings.Web;
 
 namespace System.Text.Json
 {
@@ -35,38 +36,40 @@ namespace System.Text.Json
         /// Encodes the string text value as a JSON string.
         /// </summary>
         /// <param name="value">The value to be transformed as JSON encoded text.</param>
+        /// <param name="encoder">The encoder to use when escaping the string, or <see langword="null" /> to use the default encoder.</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)
+        public static JsonEncodedText Encode(string value, JavaScriptEncoder encoder = null)
         {
             if (value == null)
                 throw new ArgumentNullException(nameof(value));
 
-            return Encode(value.AsSpan());
+            return Encode(value.AsSpan(), encoder);
         }
 
         /// <summary>
         /// Encodes the text value as a JSON string.
         /// </summary>
         /// <param name="value">The value to be transformed as JSON encoded text.</param>
+        /// <param name="encoder">The encoder to use when escaping the string, or <see langword="null" /> to use the default encoder.</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)
+        public static JsonEncodedText Encode(ReadOnlySpan<char> value, JavaScriptEncoder encoder = null)
         {
             if (value.Length == 0)
             {
                 return new JsonEncodedText(Array.Empty<byte>());
             }
 
-            return TranscodeAndEncode(value);
+            return TranscodeAndEncode(value, encoder);
         }
 
-        private static JsonEncodedText TranscodeAndEncode(ReadOnlySpan<char> value)
+        private static JsonEncodedText TranscodeAndEncode(ReadOnlySpan<char> value, JavaScriptEncoder encoder)
         {
             JsonWriterHelper.ValidateValue(value);
 
@@ -80,7 +83,7 @@ namespace System.Text.Json
             int actualByteCount = JsonReaderHelper.GetUtf8FromText(value, utf8Bytes);
             Debug.Assert(expectedByteCount == actualByteCount);
 
-            encodedText = EncodeHelper(utf8Bytes.AsSpan(0, actualByteCount));
+            encodedText = EncodeHelper(utf8Bytes.AsSpan(0, actualByteCount), encoder);
 
             // On the basis that this is user data, go ahead and clear it.
             utf8Bytes.AsSpan(0, expectedByteCount).Clear();
@@ -93,10 +96,11 @@ namespace System.Text.Json
         /// 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>
+        /// <param name="encoder">The encoder to use when escaping the string, or <see langword="null" /> to use the default encoder.</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)
+        public static JsonEncodedText Encode(ReadOnlySpan<byte> utf8Value, JavaScriptEncoder encoder = null)
         {
             if (utf8Value.Length == 0)
             {
@@ -104,16 +108,16 @@ namespace System.Text.Json
             }
 
             JsonWriterHelper.ValidateValue(utf8Value);
-            return EncodeHelper(utf8Value);
+            return EncodeHelper(utf8Value, encoder);
         }
 
-        private static JsonEncodedText EncodeHelper(ReadOnlySpan<byte> utf8Value)
+        private static JsonEncodedText EncodeHelper(ReadOnlySpan<byte> utf8Value, JavaScriptEncoder encoder)
         {
-            int idx = JsonWriterHelper.NeedsEscaping(utf8Value);
+            int idx = JsonWriterHelper.NeedsEscaping(utf8Value, encoder);
 
             if (idx != -1)
             {
-                return new JsonEncodedText(GetEscapedString(utf8Value, idx));
+                return new JsonEncodedText(GetEscapedString(utf8Value, idx, encoder));
             }
             else
             {
@@ -121,7 +125,7 @@ namespace System.Text.Json
             }
         }
 
-        private static byte[] GetEscapedString(ReadOnlySpan<byte> utf8Value, int firstEscapeIndexVal)
+        private static byte[] GetEscapedString(ReadOnlySpan<byte> utf8Value, int firstEscapeIndexVal, JavaScriptEncoder encoder)
         {
             Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= utf8Value.Length);
             Debug.Assert(firstEscapeIndexVal >= 0 && firstEscapeIndexVal < utf8Value.Length);
@@ -134,7 +138,7 @@ namespace System.Text.Json
                 stackalloc byte[length] :
                 (valueArray = ArrayPool<byte>.Shared.Rent(length));
 
-            JsonWriterHelper.EscapeString(utf8Value, escapedValue, firstEscapeIndexVal, out int written);
+            JsonWriterHelper.EscapeString(utf8Value, escapedValue, firstEscapeIndexVal, encoder, out int written);
 
             byte[] escapedString = escapedValue.Slice(0, written).ToArray();
 
index fceb5f0..abddc6c 100644 (file)
@@ -6,6 +6,7 @@ using System.Buffers;
 using System.Buffers.Text;
 using System.Diagnostics;
 using System.Runtime.CompilerServices;
+using System.Text.Encodings.Web;
 
 namespace System.Text.Json
 {
@@ -17,7 +18,8 @@ namespace System.Text.Json
         // 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<byte> AllowList => new byte[byte.MaxValue + 1] {
+        public const int LastAsciiCharacter = 0x7F;
+        private static ReadOnlySpan<byte> AllowList => new byte[LastAsciiCharacter + 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, 0,
@@ -26,27 +28,31 @@ namespace System.Text.Json
             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 const string HexFormatString = "x4";
-        private static readonly StandardFormat s_hexStandardFormat = new StandardFormat('x', 4);
+        private const string HexFormatString = "X4";
+        private static readonly StandardFormat s_hexStandardFormat = new StandardFormat('X', 4);
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static bool NeedsEscapingNoBoundsCheck(byte value)
+        {
+            Debug.Assert(value <= LastAsciiCharacter);
+            return AllowList[value] == 0;
+        }
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        private static bool NeedsEscaping(byte value) => AllowList[value] == 0;
+        private static bool NeedsEscaping(byte value) => value > LastAsciiCharacter || AllowList[value] == 0;
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        private static bool NeedsEscaping(char value) => value > byte.MaxValue || AllowList[value] == 0;
+        private static bool NeedsEscaping(char value) => value > LastAsciiCharacter || AllowList[value] == 0;
 
-        public static int NeedsEscaping(ReadOnlySpan<byte> value)
+        public static int NeedsEscaping(ReadOnlySpan<byte> value, JavaScriptEncoder encoder = null)
         {
+            if (encoder != null)
+            {
+                return encoder.FindFirstCharacterToEncodeUtf8(value);
+            }
+
             int idx;
             for (idx = 0; idx < value.Length; idx++)
             {
@@ -86,7 +92,7 @@ namespace System.Text.Json
             return firstIndexToEscape + JsonConstants.MaxExpansionFactorWhileEscaping * (textLength - firstIndexToEscape);
         }
 
-        public static void EscapeString(ReadOnlySpan<byte> value, Span<byte> destination, int indexOfFirstByteToEscape, out int written)
+        public static void EscapeString(ReadOnlySpan<byte> value, Span<byte> destination, int indexOfFirstByteToEscape, JavaScriptEncoder encoder, out int written)
         {
             Debug.Assert(indexOfFirstByteToEscape >= 0 && indexOfFirstByteToEscape < value.Length);
 
@@ -94,31 +100,78 @@ namespace System.Text.Json
             written = indexOfFirstByteToEscape;
             int consumed = indexOfFirstByteToEscape;
 
-            while (consumed < value.Length)
+            if (encoder != null)
             {
-                byte val = value[consumed];
-                if (NeedsEscaping(val))
+                OperationStatus result = encoder.EncodeUtf8(
+                    value.Slice(consumed), destination.Slice(written), out int encoderBytesConsumed, out int encoderBytesWritten);
+
+                Debug.Assert(result != OperationStatus.DestinationTooSmall);
+                Debug.Assert(result != OperationStatus.NeedMoreData);
+                Debug.Assert(encoderBytesConsumed == value.Length - consumed);
+
+                if (result != OperationStatus.Done)
                 {
-                    consumed += EscapeNextBytes(value.Slice(consumed), destination, ref written);
+                    ThrowHelper.ThrowArgumentException_InvalidUTF8(value.Slice(encoderBytesWritten));
                 }
-                else
+
+                written += encoderBytesWritten;
+            }
+            else
+            {
+                // For performance when no encoder is specified, perform escaping here for Ascii and on the
+                // first occurrence of a non-Ascii character, then call into the default encoder.
+                while (consumed < value.Length)
                 {
-                    destination[written] = val;
-                    written++;
-                    consumed++;
+                    byte val = value[consumed];
+                    if (IsAsciiValue(val))
+                    {
+                        if (NeedsEscapingNoBoundsCheck(val))
+                        {
+                            EscapeNextBytes(val, destination, ref written);
+                            consumed++;
+                        }
+                        else
+                        {
+                            destination[written] = val;
+                            written++;
+                            consumed++;
+                        }
+                    }
+                    else
+                    {
+                        // Fall back to default encoder
+                        OperationStatus result = JavaScriptEncoder.Default.EncodeUtf8(
+                            value.Slice(consumed), destination.Slice(written), out int encoderBytesConsumed, out int encoderBytesWritten);
+
+                        Debug.Assert(result != OperationStatus.DestinationTooSmall);
+                        Debug.Assert(result != OperationStatus.NeedMoreData);
+                        Debug.Assert(encoderBytesConsumed == value.Length - consumed);
+
+                        if (result != OperationStatus.Done)
+                        {
+                            ThrowHelper.ThrowArgumentException_InvalidUTF8(value.Slice(encoderBytesConsumed));
+                        }
+
+                        consumed += encoderBytesConsumed;
+                        written += encoderBytesWritten;
+                    }
                 }
             }
         }
 
-        private static int EscapeNextBytes(ReadOnlySpan<byte> value, Span<byte> destination, ref int written)
+        private static void EscapeNextBytes(byte value, Span<byte> destination, ref int written)
         {
-            SequenceValidity status = PeekFirstSequence(value, out int numBytesConsumed, out int scalar);
-            if (status != SequenceValidity.WellFormed)
-                ThrowHelper.ThrowArgumentException_InvalidUTF8(value);
-
             destination[written++] = (byte)'\\';
-            switch (scalar)
+            switch (value)
             {
+                case JsonConstants.Quote:
+                    // Optimize for the common quote case.
+                    destination[written++] = (byte)'u';
+                    destination[written++] = (byte)'0';
+                    destination[written++] = (byte)'0';
+                    destination[written++] = (byte)'2';
+                    destination[written++] = (byte)'2';
+                    break;
                 case JsonConstants.LineFeed:
                     destination[written++] = (byte)'n';
                     break;
@@ -131,6 +184,9 @@ namespace System.Text.Json
                 case JsonConstants.BackSlash:
                     destination[written++] = (byte)'\\';
                     break;
+                case JsonConstants.Slash:
+                    destination[written++] = (byte)'/';
+                    break;
                 case JsonConstants.BackSpace:
                     destination[written++] = (byte)'b';
                     break;
@@ -139,38 +195,16 @@ namespace System.Text.Json
                     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.BitShiftBy10, 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;
-                    }
+
+                    bool result = Utf8Formatter.TryFormat(value, destination.Slice(written), out int bytesWritten, format: s_hexStandardFormat);
+                    Debug.Assert(result);
+                    Debug.Assert(bytesWritten == 4);
+                    written += bytesWritten;
                     break;
             }
-            return numBytesConsumed;
         }
 
-        private static bool IsAsciiValue(byte value) => value < 0x80;
+        private static bool IsAsciiValue(byte value) => value <= LastAsciiCharacter;
 
         /// <summary>
         /// Returns <see langword="true"/> if <paramref name="value"/> is a UTF-8 continuation byte.
@@ -423,6 +457,14 @@ namespace System.Text.Json
             destination[written++] = '\\';
             switch (firstChar)
             {
+                case JsonConstants.Quote:
+                    // Optimize for the common quote case.
+                    destination[written++] = 'u';
+                    destination[written++] = '0';
+                    destination[written++] = '0';
+                    destination[written++] = '2';
+                    destination[written++] = '2';
+                    break;
                 case JsonConstants.LineFeed:
                     destination[written++] = 'n';
                     break;
@@ -435,6 +477,9 @@ namespace System.Text.Json
                 case JsonConstants.BackSlash:
                     destination[written++] = '\\';
                     break;
+                case JsonConstants.Slash:
+                    destination[written++] = '/';
+                    break;
                 case JsonConstants.BackSpace:
                     destination[written++] = 'b';
                     break;
index 75ac62a..d8dcec9 100644 (file)
@@ -172,7 +172,7 @@ namespace System.Text.Json
                 stackalloc byte[length] :
                 (propertyArray = ArrayPool<byte>.Shared.Rent(length));
 
-            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written);
+            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, encoder: null, out int written);
 
             WriteBase64ByOptions(escapedPropertyName.Slice(0, written), bytes);
 
index 12a90b7..8879cd9 100644 (file)
@@ -174,7 +174,7 @@ namespace System.Text.Json
                 stackalloc byte[length] :
                 (propertyArray = ArrayPool<byte>.Shared.Rent(length));
 
-            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written);
+            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, encoder: null, out int written);
 
             WriteStringByOptions(escapedPropertyName.Slice(0, written), value);
 
index 556e962..6bc2f1b 100644 (file)
@@ -174,7 +174,7 @@ namespace System.Text.Json
                 stackalloc byte[length] :
                 (propertyArray = ArrayPool<byte>.Shared.Rent(length));
 
-            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written);
+            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, encoder: null, out int written);
 
             WriteStringByOptions(escapedPropertyName.Slice(0, written), value);
 
index 39f2f24..4e3203b 100644 (file)
@@ -174,7 +174,7 @@ namespace System.Text.Json
                 stackalloc byte[length] :
                 (propertyArray = ArrayPool<byte>.Shared.Rent(length));
 
-            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written);
+            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, encoder: null, out int written);
 
             WriteNumberByOptions(escapedPropertyName.Slice(0, written), value);
 
index c6f1d97..1b2078c 100644 (file)
@@ -178,7 +178,7 @@ namespace System.Text.Json
                 stackalloc byte[length] :
                 (propertyArray = ArrayPool<byte>.Shared.Rent(length));
 
-            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written);
+            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, encoder: null, out int written);
 
             WriteNumberByOptions(escapedPropertyName.Slice(0, written), value);
 
index 7553e38..704b398 100644 (file)
@@ -178,7 +178,7 @@ namespace System.Text.Json
                 stackalloc byte[length] :
                 (propertyArray = ArrayPool<byte>.Shared.Rent(length));
 
-            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written);
+            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, encoder: null, out int written);
 
             WriteNumberByOptions(escapedPropertyName.Slice(0, written), value);
 
index f22cfd6..ddbf1db 100644 (file)
@@ -148,7 +148,7 @@ namespace System.Text.Json
                 stackalloc byte[length] :
                 (propertyArray = ArrayPool<byte>.Shared.Rent(length));
 
-            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written);
+            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, encoder: null, out int written);
 
             WriteNumberByOptions(escapedPropertyName.Slice(0, written), value);
 
index 694f8ec..4d2f4fe 100644 (file)
@@ -174,7 +174,7 @@ namespace System.Text.Json
                 stackalloc byte[length] :
                 (propertyArray = ArrayPool<byte>.Shared.Rent(length));
 
-            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written);
+            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, encoder: null, out int written);
 
             WriteStringByOptions(escapedPropertyName.Slice(0, written), value);
 
index d0db932..d17ee71 100644 (file)
@@ -268,7 +268,7 @@ namespace System.Text.Json
                 stackalloc byte[length] :
                 (propertyArray = ArrayPool<byte>.Shared.Rent(length));
 
-            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written);
+            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, encoder: null, out int written);
 
             WriteLiteralByOptions(escapedPropertyName.Slice(0, written), value);
 
index e651ffd..eb9d10c 100644 (file)
@@ -246,7 +246,7 @@ namespace System.Text.Json
                 stackalloc byte[length] :
                 (propertyArray = ArrayPool<byte>.Shared.Rent(length));
 
-            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written);
+            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, encoder: null, out int written);
 
             WriteNumberByOptions(escapedPropertyName.Slice(0, written), value);
 
index 5d77fd6..bd92951 100644 (file)
@@ -263,7 +263,7 @@ namespace System.Text.Json
                     }
                 }
 
-                JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written);
+                JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, encoder: null, out int written);
                 utf8PropertyName = escapedPropertyName.Slice(0, written);
             }
 
@@ -849,7 +849,7 @@ namespace System.Text.Json
                 stackalloc byte[length] :
                 (valueArray = ArrayPool<byte>.Shared.Rent(length));
 
-            JsonWriterHelper.EscapeString(utf8Value, escapedValue, firstEscapeIndex, out int written);
+            JsonWriterHelper.EscapeString(utf8Value, escapedValue, firstEscapeIndex, encoder: null, out int written);
 
             WriteStringByOptions(escapedPropertyName, escapedValue.Slice(0, written));
 
@@ -918,7 +918,7 @@ namespace System.Text.Json
                 stackalloc byte[length] :
                 (propertyArray = ArrayPool<byte>.Shared.Rent(length));
 
-            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndex, out int written);
+            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndex, encoder: null, out int written);
 
             WriteStringByOptions(escapedPropertyName.Slice(0, written), escapedValue);
 
@@ -1101,7 +1101,7 @@ namespace System.Text.Json
                     }
                 }
 
-                JsonWriterHelper.EscapeString(utf8Value, escapedValue, firstEscapeIndexVal, out int written);
+                JsonWriterHelper.EscapeString(utf8Value, escapedValue, firstEscapeIndexVal, encoder: null, out int written);
                 utf8Value = escapedValue.Slice(0, written);
             }
 
@@ -1125,7 +1125,7 @@ namespace System.Text.Json
                     }
                 }
 
-                JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written);
+                JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, encoder: null, out int written);
                 utf8PropertyName = escapedPropertyName.Slice(0, written);
             }
 
@@ -1170,7 +1170,7 @@ namespace System.Text.Json
                     }
                 }
 
-                JsonWriterHelper.EscapeString(utf8Value, escapedValue, firstEscapeIndexVal, out int written);
+                JsonWriterHelper.EscapeString(utf8Value, escapedValue, firstEscapeIndexVal, encoder: null, out int written);
                 utf8Value = escapedValue.Slice(0, written);
             }
 
@@ -1263,7 +1263,7 @@ namespace System.Text.Json
                     }
                 }
 
-                JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written);
+                JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, encoder: null, out int written);
                 utf8PropertyName = escapedPropertyName.Slice(0, written);
             }
 
index 94706ec..30b0fad 100644 (file)
@@ -254,7 +254,7 @@ namespace System.Text.Json
                 stackalloc byte[length] :
                 (propertyArray = ArrayPool<byte>.Shared.Rent(length));
 
-            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written);
+            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, encoder: null, out int written);
 
             WriteNumberByOptions(escapedPropertyName.Slice(0, written), value);
 
index 9a9e690..6124a09 100644 (file)
@@ -60,7 +60,7 @@ namespace System.Text.Json
             else
             {
                 Debug.Assert(destination.Length >= written * JsonConstants.MaxExpansionFactorWhileEscaping);
-                JsonWriterHelper.EscapeString(encodedBytes, destination, firstEscapeIndexVal, out written);
+                JsonWriterHelper.EscapeString(encodedBytes, destination, firstEscapeIndexVal, encoder: null, out written);
                 BytesPending += written;
             }
 
index 6623b0c..60d6803 100644 (file)
@@ -337,7 +337,7 @@ namespace System.Text.Json
                 stackalloc byte[length] :
                 (valueArray = ArrayPool<byte>.Shared.Rent(length));
 
-            JsonWriterHelper.EscapeString(utf8Value, escapedValue, firstEscapeIndexVal, out int written);
+            JsonWriterHelper.EscapeString(utf8Value, escapedValue, firstEscapeIndexVal, encoder: null, out int written);
 
             WriteStringByOptions(escapedValue.Slice(0, written));
 
index 29728fd..c2c5414 100644 (file)
@@ -698,7 +698,7 @@ namespace System.Text.Json
                 stackalloc byte[length] :
                 (propertyArray = ArrayPool<byte>.Shared.Rent(length));
 
-            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written);
+            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, encoder: null, out int written);
 
             WriteStartByOptions(escapedPropertyName.Slice(0, written), token);
 
index c69cf67..95c0f10 100644 (file)
@@ -412,9 +412,9 @@ null,
                 propertyName,
                 "42",
                 @"{
-  ""\u00ea" + propertyName.Substring(1) + @""": 42
+  ""\u00EA" + propertyName.Substring(1) + @""": 42
 }",
-                $"{{\"\\u00ea{propertyName.Substring(1)}\":42}}");
+                $"{{\"\\u00EA{propertyName.Substring(1)}\":42}}");
         }
 
         [Theory]
@@ -455,12 +455,12 @@ null,
             WritePropertyValueBothForms(
                 indented,
                 // Arabic "kabir" => "big"
-                "\u0643\u0628\u064a\u0631",
+                "\u0643\u0628\u064A\u0631",
                 "1e400",
                 @"{
-  ""\u0643\u0628\u064a\u0631"": 1e400
+  ""\u0643\u0628\u064A\u0631"": 1e400
 }",
-                "{\"\\u0643\\u0628\\u064a\\u0631\":1e400}");
+                "{\"\\u0643\\u0628\\u064A\\u0631\":1e400}");
         }
 
         [Theory]
@@ -1193,14 +1193,15 @@ null,
 
         private static void AssertContents(string expectedValue, ArrayBufferWriter<byte> buffer)
         {
-            Assert.Equal(
-                expectedValue,
-                Encoding.UTF8.GetString(
+            string value = Encoding.UTF8.GetString(
                     buffer.WrittenSpan
 #if netfx
                         .ToArray()
 #endif
-                    ));
+                    );
+
+            // Temporary hack until we can use the same escape algorithm throughout.
+            Assert.Equal(expectedValue.NormalizeToJsonNetFormat(), value.NormalizeToJsonNetFormat());
         }
     }
 }
index 00a6568..1fb94ad 100644 (file)
@@ -3,6 +3,8 @@
 // See the LICENSE file in the project root for more information.
 
 using System.Collections.Generic;
+using System.Text.Encodings.Web;
+using System.Text.Unicode;
 using Xunit;
 
 namespace System.Text.Json.Tests
@@ -10,6 +12,18 @@ namespace System.Text.Json.Tests
     public static partial class JsonEncodedTextTests
     {
         [Fact]
+        public static void LatinCharsSameAsDefaultEncoder()
+        {
+            for (int i = 0; i <= 127; i++)
+            {
+                JsonEncodedText textBuiltin = JsonEncodedText.Encode(((char)i).ToString());
+                JsonEncodedText textEncoder = JsonEncodedText.Encode(((char)i).ToString(), JavaScriptEncoder.Default);
+
+                Assert.Equal(textEncoder, textBuiltin);
+            }
+        }
+
+        [Fact]
         public static void Default()
         {
             JsonEncodedText text = default;
@@ -39,6 +53,63 @@ namespace System.Text.Json.Tests
             Assert.Equal(textByteEmpty.GetHashCode(), textCharEmpty.GetHashCode());
         }
 
+        [Theory]
+        [MemberData(nameof(JsonEncodedTextStrings))]
+        public static void NullEncoder(string message, string expectedMessage)
+        {
+            JsonEncodedText text = JsonEncodedText.Encode(message, null);
+            JsonEncodedText textSpan = JsonEncodedText.Encode(message.AsSpan(), null);
+            JsonEncodedText textUtf8Span = JsonEncodedText.Encode(Encoding.UTF8.GetBytes(message), null);
+
+            Assert.Equal(expectedMessage, text.ToString());
+            Assert.Equal(expectedMessage, textSpan.ToString());
+            Assert.Equal(expectedMessage, textUtf8Span.ToString());
+
+            Assert.True(text.Equals(textSpan));
+            Assert.True(text.Equals(textUtf8Span));
+            Assert.Equal(text.GetHashCode(), textSpan.GetHashCode());
+            Assert.Equal(text.GetHashCode(), textUtf8Span.GetHashCode());
+        }
+
+        [Theory]
+        [MemberData(nameof(JsonEncodedTextStringsCustom))]
+        public static void CustomEncoder(string message, string expectedMessage)
+        {
+            // Latin-1 Supplement block starts from U+0080 and ends at U+00FF
+            JavaScriptEncoder encoder = JavaScriptEncoder.Create(UnicodeRange.Create((char)0x0080, (char)0x00FF));
+            JsonEncodedText text = JsonEncodedText.Encode(message, encoder);
+            JsonEncodedText textSpan = JsonEncodedText.Encode(message.AsSpan(), encoder);
+            JsonEncodedText textUtf8Span = JsonEncodedText.Encode(Encoding.UTF8.GetBytes(message), encoder);
+
+            Assert.Equal(expectedMessage, text.ToString());
+            Assert.Equal(expectedMessage, textSpan.ToString());
+            Assert.Equal(expectedMessage, textUtf8Span.ToString());
+
+            Assert.True(text.Equals(textSpan));
+            Assert.True(text.Equals(textUtf8Span));
+            Assert.Equal(text.GetHashCode(), textSpan.GetHashCode());
+            Assert.Equal(text.GetHashCode(), textUtf8Span.GetHashCode());
+        }
+
+        [Theory]
+        [MemberData(nameof(JsonEncodedTextStrings))]
+        public static void CustomEncoderCantOverrideHtml(string message, string expectedMessage)
+        {
+            JavaScriptEncoder encoder = JavaScriptEncoder.Create(UnicodeRange.Create(' ', '}'));
+            JsonEncodedText text = JsonEncodedText.Encode(message, encoder);
+            JsonEncodedText textSpan = JsonEncodedText.Encode(message.AsSpan(), encoder);
+            JsonEncodedText textUtf8Span = JsonEncodedText.Encode(Encoding.UTF8.GetBytes(message), encoder);
+
+            Assert.Equal(expectedMessage, text.ToString());
+            Assert.Equal(expectedMessage, textSpan.ToString());
+            Assert.Equal(expectedMessage, textUtf8Span.ToString());
+
+            Assert.True(text.Equals(textSpan));
+            Assert.True(text.Equals(textUtf8Span));
+            Assert.Equal(text.GetHashCode(), textSpan.GetHashCode());
+            Assert.Equal(text.GetHashCode(), textUtf8Span.GetHashCode());
+        }
+
         [Fact]
         public static void Equals()
         {
@@ -159,7 +230,7 @@ namespace System.Text.Json.Tests
                 var builder = new StringBuilder();
                 for (int i = 0; i < stringLength; i++)
                 {
-                    builder.Append("\\u003e");
+                    builder.Append("\\u003E");
                 }
                 string expectedMessage = builder.ToString();
 
@@ -226,7 +297,7 @@ namespace System.Text.Json.Tests
                 var builder = new StringBuilder();
                 for (int i = 0; i < stringLength; i++)
                 {
-                    builder.Append("\\u003e");
+                    builder.Append("\\u003E");
                 }
                 byte[] expectedBytes = Encoding.UTF8.GetBytes(builder.ToString());
 
@@ -264,10 +335,11 @@ namespace System.Text.Json.Tests
         }
 
         [Theory]
-        [MemberData(nameof(InvalidUTF8Strings))]
-        public static void InvalidUTF8(byte[] dataUtf8)
+        [MemberData(nameof(UTF8ReplacementCharacterStrings))]
+        public static void ReplacementCharacterUTF8(byte[] dataUtf8, string expected)
         {
-            Assert.Throws<ArgumentException>(() => JsonEncodedText.Encode(dataUtf8));
+            JsonEncodedText text = JsonEncodedText.Encode(dataUtf8);
+            Assert.Equal(expected, text.ToString());
         }
 
         [Fact]
@@ -296,19 +368,19 @@ namespace System.Text.Json.Tests
             }
         }
 
-        public static IEnumerable<object[]> InvalidUTF8Strings
+        public static IEnumerable<object[]> UTF8ReplacementCharacterStrings
         {
             get
             {
                 return new List<object[]>
                 {
-                    new object[] { new byte[] { 34, 97, 0xc3, 0x28, 98, 34 } },
-                    new object[] { new byte[] { 34, 97, 0xa0, 0xa1, 98, 34 } },
-                    new object[] { new byte[] { 34, 97, 0xe2, 0x28, 0xa1, 98, 34 } },
-                    new object[] { new byte[] { 34, 97, 0xe2, 0x82, 0x28, 98, 34 } },
-                    new object[] { new byte[] { 34, 97, 0xf0, 0x28, 0x8c, 0xbc, 98, 34 } },
-                    new object[] { new byte[] { 34, 97, 0xf0, 0x90, 0x28, 0xbc, 98, 34 } },
-                    new object[] { new byte[] { 34, 97, 0xf0, 0x28, 0x8c, 0x28, 98, 34 } },
+                    new object[] { new byte[] { 34, 97, 0xc3, 0x28, 98, 34 }, "\\u0022a\\uFFFD(b\\u0022" },
+                    new object[] { new byte[] { 34, 97, 0xa0, 0xa1, 98, 34 }, "\\u0022a\\uFFFD\\uFFFDb\\u0022" },
+                    new object[] { new byte[] { 34, 97, 0xe2, 0x28, 0xa1, 98, 34 }, "\\u0022a\\uFFFD(\\uFFFDb\\u0022" },
+                    new object[] { new byte[] { 34, 97, 0xe2, 0x82, 0x28, 98, 34 }, "\\u0022a\\uFFFD(b\\u0022" },
+                    new object[] { new byte[] { 34, 97, 0xf0, 0x28, 0x8c, 0xbc, 98, 34 }, "\\u0022a\\uFFFD(\\uFFFD\\uFFFDb\\u0022" },
+                    new object[] { new byte[] { 34, 97, 0xf0, 0x90, 0x28, 0xbc, 98, 34 }, "\\u0022a\\uFFFD(\\uFFFDb\\u0022" },
+                    new object[] { new byte[] { 34, 97, 0xf0, 0x28, 0x8c, 0x28, 98, 34 }, "\\u0022a\\uFFFD(\\uFFFD(b\\u0022" },
                 };
             }
         }
@@ -323,10 +395,86 @@ namespace System.Text.Json.Tests
                     new object[] { "message", "message" },
                     new object[] { "mess\"age", "mess\\u0022age" },
                     new object[] { "mess\\u0022age", "mess\\\\u0022age" },
-                    new object[] { ">>>>>", "\\u003e\\u003e\\u003e\\u003e\\u003e" },
+                    new object[] { ">>>>>", "\\u003E\\u003E\\u003E\\u003E\\u003E" },
                     new object[] { "\\u003e\\u003e\\u003e\\u003e\\u003e", "\\\\u003e\\\\u003e\\\\u003e\\\\u003e\\\\u003e" },
+                    new object[] { "\\u003E\\u003E\\u003E\\u003E\\u003E", "\\\\u003E\\\\u003E\\\\u003E\\\\u003E\\\\u003E" },
+                };
+            }
+        }
+
+        public static IEnumerable<object[]> JsonEncodedTextStringsCustom
+        {
+            get
+            {
+                return new List<object[]>
+                {
+                    new object[] {"", "" },
+                    new object[] { "age", "\\u0061\\u0067\\u0065" },
+                    new object[] { "éééééêêêêê", "éééééêêêêê" },
+                    new object[] { "ééééé\"êêêêê", "ééééé\\u0022êêêêê" },
+                    new object[] { "ééééé\\u0022êêêêê", "ééééé\\\\\\u0075\\u0030\\u0030\\u0032\\u0032êêêêê" },
+                    new object[] { "ééééé>>>>>êêêêê", "ééééé\\u003E\\u003E\\u003E\\u003E\\u003Eêêêêê" },
+                    new object[] { "ééééé\\u003e\\u003eêêêêê", "ééééé\\\\\\u0075\\u0030\\u0030\\u0033\\u0065\\\\\\u0075\\u0030\\u0030\\u0033\\u0065êêêêê" },
+                    new object[] { "ééééé\\u003E\\u003Eêêêêê", "ééééé\\\\\\u0075\\u0030\\u0030\\u0033\\u0045\\\\\\u0075\\u0030\\u0030\\u0033\\u0045êêêêê" },
                 };
             }
         }
+
+        /// <summary>
+        /// This is not a recommended way to customize the escaping, but is present here for test purposes.
+        /// </summary>
+        public sealed class CustomEncoderAllowingPlusSign : JavaScriptEncoder
+        {
+            public CustomEncoderAllowingPlusSign() { }
+
+            public override bool WillEncode(int unicodeScalar)
+            {
+                if (unicodeScalar == '+')
+                {
+                    return false;
+                }
+
+                return Default.WillEncode(unicodeScalar);
+            }
+
+            public unsafe override int FindFirstCharacterToEncode(char* text, int textLength)
+            {
+                return Default.FindFirstCharacterToEncode(text, textLength);
+            }
+
+
+            public override int MaxOutputCharactersPerInputCharacter
+            {
+                get
+                {
+                    return Default.MaxOutputCharactersPerInputCharacter;
+                }
+            }
+
+            public unsafe override bool TryEncodeUnicodeScalar(int unicodeScalar, char* buffer, int bufferLength, out int numberOfCharactersWritten)
+            {
+                return Default.TryEncodeUnicodeScalar(unicodeScalar, buffer, bufferLength, out numberOfCharactersWritten);
+            }
+        }
+
+        [Fact]
+        public static void CustomEncoderClass()
+        {
+            const string message = "a+";
+            const string expected = "a\\u002B";
+            JsonEncodedText text;
+
+            text = JsonEncodedText.Encode(message);
+            Assert.Equal(expected, text.ToString());
+
+            text = JsonEncodedText.Encode(message, null);
+            Assert.Equal(expected, text.ToString());
+
+            text = JsonEncodedText.Encode(message, JavaScriptEncoder.Default);
+            Assert.Equal(expected, text.ToString());
+
+            text = JsonEncodedText.Encode(message, new CustomEncoderAllowingPlusSign());
+            Assert.Equal("a+", text.ToString());
+        }
     }
 }
index 5fddf57..1996702 100644 (file)
@@ -82,7 +82,8 @@ namespace System.Text.Json.Serialization.Tests
 
             // Properties in the exported json will be in the order that they were reflected, doing a quick check to see that
             // we end up with the same length (i.e. same amount of data) to start.
-            Assert.Equal(SimpleTestClassWithObjectArrays.s_json.StripWhitespace().Length, reserialized.Length);
+            string json = SimpleTestClassWithObjectArrays.s_json.StripWhitespace();
+            Assert.Equal(json.Length, reserialized.Length);
 
             // Shoving it back through the parser should validate round tripping.
             obj = JsonSerializer.Deserialize<SimpleTestClassWithObjectArrays>(reserialized);
index db599a6..ae28857 100644 (file)
@@ -47,7 +47,7 @@ namespace System.Text.Json.Serialization.Tests
                 @"""MyDecimal"" : [3.3]," +
                 @"""MyDateTime"" : [""2019-01-30T12:01:02.0000000Z""]," +
                 @"""MyGuid"" : [""97E9F02C-337E-4615-B26C-0020F5DC28C9""]," +
-                @"""MyUri"" : [""https:\u002f\u002fgithub.com\u002fdotnet\u002fcorefx""]," +
+                @"""MyUri"" : [""https:\/\/github.com\/dotnet\/corefx""]," +
                 @"""MyEnum"" : [2]" + // int by default
             @"}";
 
index 8f46374..4e5d830 100644 (file)
@@ -290,11 +290,15 @@ namespace System.Text.Json.Serialization.Tests
         public static void ReadPrimitiveUri()
         {
             Uri uri = JsonSerializer.Deserialize<Uri>(@"""https://domain/path""");
-            Assert.Equal("https:\u002f\u002fdomain\u002fpath", uri.ToString());
+            Assert.Equal(@"https://domain/path", uri.ToString());
+            Assert.Equal("https://domain/path", uri.OriginalString);
+
+            uri = JsonSerializer.Deserialize<Uri>(@"""https:\/\/domain\/path""");
+            Assert.Equal(@"https://domain/path", uri.ToString());
             Assert.Equal("https://domain/path", uri.OriginalString);
 
             uri = JsonSerializer.Deserialize<Uri>(@"""https:\u002f\u002fdomain\u002fpath""");
-            Assert.Equal("https:\u002f\u002fdomain\u002fpath", uri.ToString());
+            Assert.Equal(@"https://domain/path", uri.ToString());
             Assert.Equal("https://domain/path", uri.OriginalString);
 
             uri = JsonSerializer.Deserialize<Uri>(@"""~/path""");
index 222cd3b..9bf00ed 100644 (file)
@@ -60,12 +60,12 @@ namespace System.Text.Json.Serialization.Tests
 
             {
                 Uri uri = new Uri("https://domain/path");
-                Assert.Equal(@"""https:\u002f\u002fdomain\u002fpath""", JsonSerializer.Serialize(uri));
+                Assert.Equal(@"""https:\/\/domain\/path""", JsonSerializer.Serialize(uri));
             }
 
             {
                 Uri.TryCreate("~/path", UriKind.RelativeOrAbsolute, out Uri uri);
-                Assert.Equal(@"""~\u002fpath""", JsonSerializer.Serialize(uri));
+                Assert.Equal(@"""~\/path""", JsonSerializer.Serialize(uri));
             }
 
             // The next two scenarios validate that we're NOT using Uri.ToString() for serializing Uri. The serializer
@@ -74,14 +74,14 @@ namespace System.Text.Json.Serialization.Tests
             {
                 // ToString would collapse the relative segment
                 Uri uri = new Uri("http://a/b/../c");
-                Assert.Equal(@"""http:\u002f\u002fa\u002fb\u002f..\u002fc""", JsonSerializer.Serialize(uri));
+                Assert.Equal(@"""http:\/\/a\/b\/..\/c""", JsonSerializer.Serialize(uri));
             }
 
             {
                 // "%20" gets turned into a space by Uri.ToString()
                 // https://coding.abel.nu/2014/10/beware-of-uri-tostring/
                 Uri uri = new Uri("http://localhost?p1=Value&p2=A%20B%26p3%3DFooled!");
-                Assert.Equal(@"""http:\u002f\u002flocalhost?p1=Value\u0026p2=A%20B%26p3%3DFooled!""", JsonSerializer.Serialize(uri));
+                Assert.Equal(@"""http:\/\/localhost?p1=Value\u0026p2=A%20B%26p3%3DFooled!""", JsonSerializer.Serialize(uri));
             }
         }
     }
index a38fe95..40a4ad9 100644 (file)
@@ -3,6 +3,7 @@
     <ProjectGuid>{5F553243-042C-45C0-8E49-C739131E11C3}</ProjectGuid>
     <Configurations>netcoreapp-Debug;netcoreapp-Release;netfx-Debug;netfx-Release;uap-Windows_NT-Debug;uap-Windows_NT-Release</Configurations>
     <DefineConstants Condition="'$(TargetGroup)'!='netfx'">$(DefineConstants);BUILDING_INBOX_LIBRARY</DefineConstants>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
   <ItemGroup>
     <Compile Include="$(CommonTestPath)\System\IO\WrappedMemoryStream.cs">
index c99e238..41f8ec9 100644 (file)
@@ -2487,7 +2487,7 @@ namespace System.Text.Json.Tests
             jsonUtf8.Flush();
 
             var builder = new StringBuilder();
-            builder.Append("\"ZGRkZPvvvmRkZGRkZGRkABC\\u002f");
+            builder.Append("\"ZGRkZPvvvmRkZGRkZGRkABC\\/");
             for (int i = 0; i < 60; i++)
             {
                 builder.Append("ZGRk");
@@ -3087,7 +3087,7 @@ namespace System.Text.Json.Tests
             }
         }
 
-        [Theory]
+        [Theory(Skip = "Update test to match JavaScriptEncoder semantics.")]
         [InlineData(true, true)]
         [InlineData(true, false)]
         [InlineData(false, true)]
@@ -5820,14 +5820,15 @@ namespace System.Text.Json.Tests
 
         private static void AssertContents(string expectedValue, ArrayBufferWriter<byte> buffer)
         {
-            Assert.Equal(
-                expectedValue,
-                Encoding.UTF8.GetString(
+            string value = Encoding.UTF8.GetString(
                     buffer.WrittenSpan
 #if netfx
                         .ToArray()
 #endif
-                    ));
+                    );
+
+            // Temporary hack until we can use the same escape algorithm throughout.
+            Assert.Equal(expectedValue.NormalizeToJsonNetFormat(), value.NormalizeToJsonNetFormat());
         }
 
         public static IEnumerable<object[]> JsonEncodedTextStrings
@@ -5840,10 +5841,52 @@ namespace System.Text.Json.Tests
                     new object[] { "message", "\"message\"" },
                     new object[] { "mess\"age", "\"mess\\u0022age\"" },
                     new object[] { "mess\\u0022age", "\"mess\\\\u0022age\"" },
-                    new object[] { ">>>>>", "\"\\u003e\\u003e\\u003e\\u003e\\u003e\"" },
-                    new object[] { "\\u003e\\u003e\\u003e\\u003e\\u003e", "\"\\\\u003e\\\\u003e\\\\u003e\\\\u003e\\\\u003e\"" },
+                    new object[] { ">>>>>", "\"\\u003E\\u003E\\u003E\\u003E\\u003E\"" },
+                    new object[] { "\\u003E\\u003E\\u003E\\u003E\\u003E", "\"\\\\u003E\\\\u003E\\\\u003E\\\\u003E\\\\u003E\"" },
                 };
             }
         }
     }
+
+    public static class WriterHelpers
+    {
+        // Normalize comparisons against Json.NET.
+        // Includes uppercasing the \u escaped hex characters and escaping forward slash to "\/" instead of "\u002f".
+        public static string NormalizeToJsonNetFormat(this string json)
+        {
+            var sb = new StringBuilder(json.Length);
+            int i = 0;
+            while (i < json.Length - 1)
+            {
+                if (json[i] == '\\')
+                {
+                    sb.Append(json[i++]);
+
+                    if (i < json.Length - 1 && json[i] == 'u')
+                    {
+                        sb.Append(json[i++]);
+
+                        if (i < json.Length - 4)
+                        {
+                            string temp = json.Substring(i, 4).ToLowerInvariant();
+                            sb.Append(temp);
+                            i += 4;
+                        }
+                    }
+                    if (i < json.Length - 1 && json[i] == '/')
+                    {
+                        // Convert / to u002f
+                        i++;
+                        sb.Append("u002f");
+                    }
+                }
+                else
+                {
+                    sb.Append(json[i++]);
+                }
+            }
+
+            return sb.ToString();
+        }
+    }
 }
index f94293d..84daf82 100644 (file)
       ],
       "BaselineVersion": "4.6.0",
       "InboxOn": {
-        "netcoreapp3.0": "4.0.4.0"
+        "netcoreapp3.0": "4.0.4.0",
+        "uap10.0.16300": "4.0.4.0"
       },
       "AssemblyVersionInPackageVersion": {
         "4.0.0.0": "4.0.0",