Add Base64 APIs to Utf8JsonReader, Utf8JsonWriter, and JsonElement (dotnet/corefx...
authorAhson Khan <ahkha@microsoft.com>
Fri, 31 May 2019 05:13:31 +0000 (22:13 -0700)
committerGitHub <noreply@github.com>
Fri, 31 May 2019 05:13:31 +0000 (22:13 -0700)
* Add Utf8JsonWriter Base64 APIs.

* Add Utf8JsonReader Base64 APIs.

* Add JsonElement Base64 APIs.

* Update API shape based on review.

* Auto-generate the ref assembly.

* Address PR feedback so far.

* Add escaping step and update length counters accordingly.

* Add JsonWriter API tests.

* Add JsonReader and JsonElement tests.

Commit migrated from https://github.com/dotnet/corefx/commit/82408cd90f4d4573d502e8df2ca437b35e6a37f7

17 files changed:
src/libraries/System.Text.Json/ref/System.Text.Json.cs
src/libraries/System.Text.Json/src/Resources/Strings.resx
src/libraries/System.Text.Json/src/System.Text.Json.csproj
src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs
src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs
src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs
src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.Unescaping.cs
src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs
src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/JsonWriterHelper.Escaping.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/JsonWriterHelper.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Bytes.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Bytes.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Helpers.cs
src/libraries/System.Text.Json/tests/JsonDocumentTests.cs
src/libraries/System.Text.Json/tests/Utf8JsonReaderTests.TryGet.cs
src/libraries/System.Text.Json/tests/Utf8JsonWriterTests.cs

index fbf9ea8..664f72a 100644 (file)
@@ -38,6 +38,7 @@ namespace System.Text.Json
         public System.Text.Json.JsonElement.ObjectEnumerator EnumerateObject() { throw null; }
         public int GetArrayLength() { throw null; }
         public bool GetBoolean() { throw null; }
+        public byte[] GetBytesFromBase64() { throw null; }
         public System.DateTime GetDateTime() { throw null; }
         public System.DateTimeOffset GetDateTimeOffset() { throw null; }
         public decimal GetDecimal() { throw null; }
@@ -56,6 +57,7 @@ namespace System.Text.Json
         [System.CLSCompliantAttribute(false)]
         public ulong GetUInt64() { throw null; }
         public override string ToString() { throw null; }
+        public bool TryGetBytesFromBase64(out byte[] value) { throw null; }
         public bool TryGetDateTime(out System.DateTime value) { throw null; }
         public bool TryGetDateTimeOffset(out System.DateTimeOffset value) { throw null; }
         public bool TryGetDecimal(out decimal value) { throw null; }
@@ -196,9 +198,10 @@ namespace System.Text.Json
         public System.Buffers.ReadOnlySequence<byte> ValueSequence { get { throw null; } }
         public System.ReadOnlySpan<byte> ValueSpan { get { throw null; } }
         public bool GetBoolean() { throw null; }
+        public byte[] GetBytesFromBase64() { throw null; }
+        public string GetComment() { throw null; }
         public System.DateTime GetDateTime() { throw null; }
         public System.DateTimeOffset GetDateTimeOffset() { throw null; }
-        public string GetComment() { throw null; }
         public decimal GetDecimal() { throw null; }
         public double GetDouble() { throw null; }
         public System.Guid GetGuid() { throw null; }
@@ -214,6 +217,7 @@ namespace System.Text.Json
         public void Skip() { }
         public bool TextEquals(System.ReadOnlySpan<byte> otherUtf8Text) { throw null; }
         public bool TextEquals(System.ReadOnlySpan<char> otherText) { throw null; }
+        public bool TryGetBytesFromBase64(out byte[] value) { throw null; }
         public bool TryGetDateTime(out System.DateTime value) { throw null; }
         public bool TryGetDateTimeOffset(out System.DateTimeOffset value) { throw null; }
         public bool TryGetDecimal(out decimal value) { throw null; }
@@ -242,6 +246,11 @@ namespace System.Text.Json
         public void Reset() { }
         public void Reset(System.Buffers.IBufferWriter<byte> bufferWriter) { }
         public void Reset(System.IO.Stream utf8Json) { }
+        public void WriteBase64String(System.ReadOnlySpan<byte> utf8PropertyName, System.ReadOnlySpan<byte> bytes) { }
+        public void WriteBase64String(System.ReadOnlySpan<char> propertyName, System.ReadOnlySpan<byte> bytes) { }
+        public void WriteBase64String(string propertyName, System.ReadOnlySpan<byte> bytes) { }
+        public void WriteBase64String(System.Text.Json.JsonEncodedText propertyName, System.ReadOnlySpan<byte> bytes) { }
+        public void WriteBase64StringValue(System.ReadOnlySpan<byte> bytes) { }
         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) { }
@@ -385,10 +394,10 @@ namespace System.Text.Json.Serialization
         public static TValue Parse<TValue>(string json, System.Text.Json.Serialization.JsonSerializerOptions options = null) { throw null; }
         public static System.Threading.Tasks.ValueTask<object> ReadAsync(System.IO.Stream utf8Json, System.Type returnType, System.Text.Json.Serialization.JsonSerializerOptions options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
         public static System.Threading.Tasks.ValueTask<TValue> ReadAsync<TValue>(System.IO.Stream utf8Json, System.Text.Json.Serialization.JsonSerializerOptions options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
-        public static byte[] ToUtf8Bytes(object value, System.Type type, System.Text.Json.Serialization.JsonSerializerOptions options = null) { throw null; }
-        public static byte[] ToUtf8Bytes<TValue>(TValue value, System.Text.Json.Serialization.JsonSerializerOptions options = null) { throw null; }
         public static string ToString(object value, System.Type type, System.Text.Json.Serialization.JsonSerializerOptions options = null) { throw null; }
         public static string ToString<TValue>(TValue value, System.Text.Json.Serialization.JsonSerializerOptions options = null) { throw null; }
+        public static byte[] ToUtf8Bytes(object value, System.Type type, System.Text.Json.Serialization.JsonSerializerOptions options = null) { throw null; }
+        public static byte[] ToUtf8Bytes<TValue>(TValue value, System.Text.Json.Serialization.JsonSerializerOptions options = null) { throw null; }
         public static System.Threading.Tasks.Task WriteAsync(object value, System.Type type, System.IO.Stream utf8Json, System.Text.Json.Serialization.JsonSerializerOptions options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
         public static System.Threading.Tasks.Task WriteAsync<TValue>(TValue value, System.IO.Stream utf8Json, System.Text.Json.Serialization.JsonSerializerOptions options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
     }
index f0d121a..6f8b9bc 100644 (file)
   <data name="CannotTranscodeInvalidUtf8" xml:space="preserve">
     <value>Cannot transcode invalid UTF-8 JSON text to UTF-16 string.</value>
   </data>
+  <data name="CannotDecodeInvalidBase64" xml:space="preserve">
+    <value>Cannot decode JSON text that is not encoded as valid Base64 to bytes.</value>
+  </data>
   <data name="CannotTranscodeInvalidUtf16" xml:space="preserve">
     <value>Cannot transcode invalid UTF-16 string to UTF-8 JSON text.</value>
   </data>
index 5bfb8e6..941b0ba 100644 (file)
@@ -11,7 +11,7 @@
     <!-- Workaround for overriding the XML comments related warnings that are being supressed repo wide (within arcade): -->
     <!-- https://github.com/dotnet/arcade/blob/ea6addfdc65e5df1b2c036f11614a5f922e36267/src/Microsoft.DotNet.Arcade.Sdk/tools/ProjectDefaults.props#L90 -->
     <!-- For this project, we want warnings if there are public APIs/types without properly formatted XML comments (particularly CS1591). -->
-    <NoWarn/>
+    <NoWarn />
   </PropertyGroup>
   <ItemGroup>
     <Compile Include="System\Text\Json\BitStack.cs" />
     <Compile Include="System\Text\Json\Writer\JsonWriterOptions.cs" />
     <Compile Include="System\Text\Json\Writer\SequenceValidity.cs" />
     <Compile Include="System\Text\Json\Writer\Utf8JsonWriter.cs" />
+    <Compile Include="System\Text\Json\Writer\Utf8JsonWriter.WriteProperties.Bytes.cs" />
     <Compile Include="System\Text\Json\Writer\Utf8JsonWriter.WriteProperties.DateTime.cs" />
     <Compile Include="System\Text\Json\Writer\Utf8JsonWriter.WriteProperties.DateTimeOffset.cs" />
     <Compile Include="System\Text\Json\Writer\Utf8JsonWriter.WriteProperties.Decimal.cs" />
     <Compile Include="System\Text\Json\Writer\Utf8JsonWriter.WriteProperties.SignedNumber.cs" />
     <Compile Include="System\Text\Json\Writer\Utf8JsonWriter.WriteProperties.String.cs" />
     <Compile Include="System\Text\Json\Writer\Utf8JsonWriter.WriteProperties.UnsignedNumber.cs" />
+    <Compile Include="System\Text\Json\Writer\Utf8JsonWriter.WriteValues.Bytes.cs" />
     <Compile Include="System\Text\Json\Writer\Utf8JsonWriter.WriteValues.Comment.cs" />
     <Compile Include="System\Text\Json\Writer\Utf8JsonWriter.WriteValues.DateTime.cs" />
     <Compile Include="System\Text\Json\Writer\Utf8JsonWriter.WriteValues.DateTimeOffset.cs" />
index 1d4e447..b5ffa48 100644 (file)
@@ -263,6 +263,29 @@ namespace System.Text.Json
             return GetString(index - DbRow.Size, JsonTokenType.PropertyName);
         }
 
+        internal bool TryGetValue(int index, out byte[] value)
+        {
+            CheckNotDisposed();
+
+            DbRow row = _parsedData.Get(index);
+
+            CheckExpectedType(JsonTokenType.String, row.TokenType);
+
+            ReadOnlySpan<byte> data = _utf8Json.Span;
+            ReadOnlySpan<byte> segment = data.Slice(row.Location, row.SizeOrLength);
+
+            // Segment needs to be unescaped
+            if (row.HasComplexChildren)
+            {
+                int idx = segment.IndexOf(JsonConstants.BackSlash);
+                Debug.Assert(idx != -1);
+                return JsonReaderHelper.TryGetUnescapedBase64Bytes(segment, idx, out value);
+            }
+
+            Debug.Assert(segment.IndexOf(JsonConstants.BackSlash) == -1);
+            return JsonReaderHelper.TryDecodeBase64(segment, out value);
+        }
+
         internal bool TryGetValue(int index, out int value)
         {
             CheckNotDisposed();
index 34be938..e9d12eb 100644 (file)
@@ -2,6 +2,7 @@
 // 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.Text;
 using System.Collections.Generic;
 using System.Diagnostics;
 
@@ -351,6 +352,57 @@ namespace System.Text.Json
         }
 
         /// <summary>
+        ///   Attempts to represent the current JSON string as bytes assuming it is base 64 encoded.
+        /// </summary>
+        /// <param name="value">Receives the value.</param>
+        /// <remarks>
+        ///  This method does not create a byte[] representation of values other than bsae 64 encoded JSON strings.
+        /// </remarks>
+        /// <returns>
+        ///   <see langword="true"/> if the entire token value is encoded as valid base 64 text and can be successfully decoded to bytes.
+        ///   <see langword="false"/> otherwise.
+        /// </returns>
+        /// <exception cref="InvalidOperationException">
+        ///   This value's <see cref="Type"/> is not <see cref="JsonValueType.String"/>.
+        /// </exception>
+        /// <exception cref="ObjectDisposedException">
+        ///   The parent <see cref="JsonDocument"/> has been disposed.
+        /// </exception>
+        public bool TryGetBytesFromBase64(out byte[] value)
+        {
+            CheckValidInstance();
+
+            return _parent.TryGetValue(_idx, out value);
+        }
+
+        /// <summary>
+        ///   Gets the value of the element as bytes.
+        /// </summary>
+        /// <remarks>
+        ///   This method does not create a byte[] representation of values other than base 64 encoded JSON strings.
+        /// </remarks>
+        /// <returns>The value decode to bytes.</returns>
+        /// <exception cref="InvalidOperationException">
+        ///   This value's <see cref="Type"/> is not <see cref="JsonValueType.String"/>.
+        /// </exception>
+        /// <exception cref="FormatException">
+        ///   The value is not encoded as base 64 text and hence cannot be decoded to bytes.
+        /// </exception>
+        /// <exception cref="ObjectDisposedException">
+        ///   The parent <see cref="JsonDocument"/> has been disposed.
+        /// </exception>
+        /// <seealso cref="ToString"/>
+        public byte[] GetBytesFromBase64()
+        {
+            if (TryGetBytesFromBase64(out byte[] value))
+            {
+                return value;
+            }
+
+            throw new FormatException();
+        }
+
+        /// <summary>
         ///   Attempts to represent the current JSON number as an <see cref="int"/>.
         /// </summary>
         /// <param name="value">Receives the value.</param>
index ec65999..bede535 100644 (file)
@@ -57,8 +57,9 @@ namespace System.Text.Json
         // All other UTF-16 characters can be represented by either 1 or 2 UTF-8 bytes.
         public const int MaxExpansionFactorWhileTranscoding = 3;
 
-        public const int MaxTokenSize = 2_000_000_000 / MaxExpansionFactorWhileEscaping;  // 357_913_941 bytes
-        public const int MaxCharacterTokenSize = 2_000_000_000 / MaxExpansionFactorWhileEscaping; // 357_913_941 characters
+        public const int MaxTokenSize = 1_000_000_000 / MaxExpansionFactorWhileEscaping;  // 166_666_666 bytes
+        public const int MaxBase46ValueTokenSize = (1_000_000_000 >> 2 * 3) / MaxExpansionFactorWhileEscaping;  // 125_000_000 bytes
+        public const int MaxCharacterTokenSize = 1_000_000_000 / MaxExpansionFactorWhileEscaping; // 166_666_666 characters
 
         public const int MaximumFormatInt64Length = 20;   // 19 + sign (i.e. -9223372036854775808)
         public const int MaximumFormatUInt64Length = 20;  // i.e. 18446744073709551615
index e7025e7..fa4502b 100644 (file)
@@ -10,6 +10,31 @@ namespace System.Text.Json
 {
     internal static partial class JsonReaderHelper
     {
+        public static bool TryGetUnescapedBase64Bytes(ReadOnlySpan<byte> utf8Source, int idx, out byte[] bytes)
+        {
+            byte[] unescapedArray = null;
+
+            Span<byte> utf8Unescaped = utf8Source.Length <= JsonConstants.StackallocThreshold ?
+                stackalloc byte[utf8Source.Length] :
+                (unescapedArray = ArrayPool<byte>.Shared.Rent(utf8Source.Length));
+
+            Unescape(utf8Source, utf8Unescaped, idx, out int written);
+            Debug.Assert(written > 0);
+
+            utf8Unescaped = utf8Unescaped.Slice(0, written);
+            Debug.Assert(!utf8Unescaped.IsEmpty);
+
+            bool result = TryDecodeBase64InPlace(utf8Unescaped, out bytes);
+
+            if (unescapedArray != null)
+            {
+                utf8Unescaped.Clear();
+                ArrayPool<byte>.Shared.Return(unescapedArray);
+            }
+
+            return result;
+        }
+
         // Reject any invalid UTF-8 data rather than silently replacing.
         public static readonly UTF8Encoding s_utf8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
 
@@ -107,6 +132,53 @@ namespace System.Text.Json
             return result;
         }
 
+        public static bool TryDecodeBase64InPlace(Span<byte> utf8Unescaped, out byte[] bytes)
+        {
+            OperationStatus status = Base64.DecodeFromUtf8InPlace(utf8Unescaped, out int bytesWritten);
+            if (status != OperationStatus.Done)
+            {
+                bytes = null;
+                return false;
+            }
+            bytes = utf8Unescaped.Slice(0, bytesWritten).ToArray();
+            return true;
+        }
+
+        public static bool TryDecodeBase64(ReadOnlySpan<byte> utf8Unescaped, out byte[] bytes)
+        {
+            byte[] pooledArray = null;
+
+            Span<byte> byteSpan = utf8Unescaped.Length <= JsonConstants.StackallocThreshold ?
+                stackalloc byte[utf8Unescaped.Length] :
+                (pooledArray = ArrayPool<byte>.Shared.Rent(utf8Unescaped.Length));
+
+            OperationStatus status = Base64.DecodeFromUtf8(utf8Unescaped, byteSpan, out int bytesConsumed, out int bytesWritten);
+
+            if (status != OperationStatus.Done)
+            {
+                bytes = null;
+
+                if (pooledArray != null)
+                {
+                    byteSpan.Clear();
+                    ArrayPool<byte>.Shared.Return(pooledArray);
+                }
+
+                return false;
+            }
+            Debug.Assert(bytesConsumed == utf8Unescaped.Length);
+
+            bytes = byteSpan.Slice(0, bytesWritten).ToArray();
+
+            if (pooledArray != null)
+            {
+                byteSpan.Clear();
+                ArrayPool<byte>.Shared.Return(pooledArray);
+            }
+
+            return true;
+        }
+
         public static string TranscodeHelper(ReadOnlySpan<byte> utf8Unescaped)
         {
             try
index d4e25a7..2151d40 100644 (file)
@@ -58,7 +58,7 @@ namespace System.Text.Json
 
         /// <summary>
         /// Parses the current JSON token value from the source as a <see cref="bool"/>.
-        /// Returns true if the TokenType is JsonTokenType.True and false if the TokenType is JsonTokenType.False.
+        /// Returns <see langword="true"/> if the TokenType is JsonTokenType.True and <see langword="false"/> if the TokenType is JsonTokenType.False.
         /// </summary>
         /// <exception cref="InvalidOperationException">
         /// Thrown if trying to get the value of a JSON token that is not a boolean (i.e. <see cref="JsonTokenType.True"/> or <see cref="JsonTokenType.False"/>).
@@ -85,6 +85,27 @@ namespace System.Text.Json
         }
 
         /// <summary>
+        /// Parses the current JSON token value from the source and decodes the base 64 encoded JSON string as bytes.
+        /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// Thrown if trying to get the value of a JSON token that is not a <see cref="JsonTokenType.String"/>.
+        /// <seealso cref="TokenType" />
+        /// </exception>
+        /// <exception cref="FormatException">
+        /// Thrown when the JSON string contains data outside of the expected base 64 range, or if it contains invalid/more than two padding characters,
+        /// or is incomplete (i.e. the JSON string length is not a multiple of 4).
+        /// </exception>
+        public byte[] GetBytesFromBase64()
+        {
+            if (!TryGetBytesFromBase64(out byte[] value))
+            {
+                throw ThrowHelper.GetFormatException(DateType.Base64String);
+            }
+
+            return value;
+        }
+
+        /// <summary>
         /// Parses the current JSON token value from the source as an <see cref="int"/>.
         /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to an <see cref="int"/>
         /// value.
@@ -321,10 +342,40 @@ namespace System.Text.Json
         }
 
         /// <summary>
+        /// Parses the current JSON token value from the source and decodes the base 64 encoded JSON string as bytes.
+        /// Returns <see langword="true"/> if the entire token value is encoded as valid base 64 text and can be successfully
+        /// decoded to bytes.
+        /// Returns <see langword="false"/> otherwise.
+        /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// Thrown if trying to get the value of a JSON token that is not a <see cref="JsonTokenType.String"/>.
+        /// <seealso cref="TokenType" />
+        /// </exception>
+        public bool TryGetBytesFromBase64(out byte[] value)
+        {
+            if (TokenType != JsonTokenType.String)
+            {
+                throw ThrowHelper.GetInvalidOperationException_ExpectedString(TokenType);
+            }
+
+            ReadOnlySpan<byte> span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;
+
+            if (_stringHasEscaping)
+            {
+                int idx = span.IndexOf(JsonConstants.BackSlash);
+                Debug.Assert(idx != -1);
+                return JsonReaderHelper.TryGetUnescapedBase64Bytes(span, idx, out value);
+            }
+
+            Debug.Assert(span.IndexOf(JsonConstants.BackSlash) == -1);
+            return JsonReaderHelper.TryDecodeBase64(span, out value);
+        }
+
+        /// <summary>
         /// Parses the current JSON token value from the source as an <see cref="int"/>.
-        /// Returns true if the entire UTF-8 encoded token value can be successfully 
+        /// Returns <see langword="true"/> if the entire UTF-8 encoded token value can be successfully 
         /// parsed to an <see cref="int"/> value.
-        /// Returns false otherwise.
+        /// Returns <see langword="false"/> otherwise.
         /// </summary>
         /// <exception cref="InvalidOperationException">
         /// Thrown if trying to get the value of a JSON token that is not a <see cref="JsonTokenType.Number"/>.
@@ -343,9 +394,9 @@ namespace System.Text.Json
 
         /// <summary>
         /// Parses the current JSON token value from the source as a <see cref="long"/>.
-        /// Returns true if the entire UTF-8 encoded token value can be successfully 
+        /// Returns <see langword="true"/> if the entire UTF-8 encoded token value can be successfully 
         /// parsed to a <see cref="long"/> value.
-        /// Returns false otherwise.
+        /// Returns <see langword="false"/> otherwise.
         /// </summary>
         /// <exception cref="InvalidOperationException">
         /// Thrown if trying to get the value of a JSON token that is not a <see cref="JsonTokenType.Number"/>.
@@ -364,9 +415,9 @@ namespace System.Text.Json
 
         /// <summary>
         /// Parses the current JSON token value from the source as a <see cref="uint"/>.
-        /// Returns true if the entire UTF-8 encoded token value can be successfully 
+        /// Returns <see langword="true"/> if the entire UTF-8 encoded token value can be successfully 
         /// parsed to a <see cref="uint"/> value.
-        /// Returns false otherwise.
+        /// Returns <see langword="false"/> otherwise.
         /// </summary>
         /// <exception cref="InvalidOperationException">
         /// Thrown if trying to get the value of a JSON token that is not a <see cref="JsonTokenType.Number"/>.
@@ -386,9 +437,9 @@ namespace System.Text.Json
 
         /// <summary>
         /// Parses the current JSON token value from the source as a <see cref="ulong"/>.
-        /// Returns true if the entire UTF-8 encoded token value can be successfully 
+        /// Returns <see langword="true"/> if the entire UTF-8 encoded token value can be successfully 
         /// parsed to a <see cref="ulong"/> value.
-        /// Returns false otherwise.
+        /// Returns <see langword="false"/> otherwise.
         /// </summary>
         /// <exception cref="InvalidOperationException">
         /// Thrown if trying to get the value of a JSON token that is not a <see cref="JsonTokenType.Number"/>.
@@ -408,9 +459,9 @@ namespace System.Text.Json
 
         /// <summary>
         /// Parses the current JSON token value from the source as a <see cref="float"/>.
-        /// Returns true if the entire UTF-8 encoded token value can be successfully 
+        /// Returns <see langword="true"/> if the entire UTF-8 encoded token value can be successfully 
         /// parsed to a <see cref="float"/> value.
-        /// Returns false otherwise.
+        /// Returns <see langword="false"/> otherwise.
         /// </summary>
         /// <exception cref="InvalidOperationException">
         /// Thrown if trying to get the value of a JSON token that is not a <see cref="JsonTokenType.Number"/>.
@@ -429,9 +480,9 @@ namespace System.Text.Json
 
         /// <summary>
         /// Parses the current JSON token value from the source as a <see cref="double"/>.
-        /// Returns true if the entire UTF-8 encoded token value can be successfully 
+        /// Returns <see langword="true"/> if the entire UTF-8 encoded token value can be successfully 
         /// parsed to a <see cref="double"/> value.
-        /// Returns false otherwise.
+        /// Returns <see langword="false"/> otherwise.
         /// </summary>
         /// <exception cref="InvalidOperationException">
         /// Thrown if trying to get the value of a JSON token that is not a <see cref="JsonTokenType.Number"/>.
@@ -450,9 +501,9 @@ namespace System.Text.Json
 
         /// <summary>
         /// Parses the current JSON token value from the source as a <see cref="decimal"/>.
-        /// Returns true if the entire UTF-8 encoded token value can be successfully 
+        /// Returns <see langword="true"/> if the entire UTF-8 encoded token value can be successfully 
         /// parsed to a <see cref="decimal"/> value.
-        /// Returns false otherwise.
+        /// Returns <see langword="false"/> otherwise.
         /// </summary>
         /// <exception cref="InvalidOperationException">
         /// Thrown if trying to get the value of a JSON token that is not a <see cref="JsonTokenType.Number"/>.
@@ -471,9 +522,9 @@ namespace System.Text.Json
 
         /// <summary>
         /// Parses the current JSON token value from the source as a <see cref="DateTime"/>.
-        /// Returns true if the entire UTF-8 encoded token value can be successfully
+        /// Returns <see langword="true"/> if the entire UTF-8 encoded token value can be successfully
         /// parsed to a <see cref="DateTime"/> value.
-        /// Returns false otherwise.
+        /// Returns <see langword="false"/> otherwise.
         /// </summary>
         /// <exception cref="InvalidOperationException">
         /// Thrown if trying to get the value of a JSON token that is not a <see cref="JsonTokenType.String"/>.
@@ -530,9 +581,9 @@ namespace System.Text.Json
 
         /// <summary>
         /// Parses the current JSON token value from the source as a <see cref="DateTimeOffset"/>.
-        /// Returns true if the entire UTF-8 encoded token value can be successfully
+        /// Returns <see langword="true"/> if the entire UTF-8 encoded token value can be successfully
         /// parsed to a <see cref="DateTimeOffset"/> value.
-        /// Returns false otherwise.
+        /// Returns <see langword="false"/> otherwise.
         /// </summary>
         /// <exception cref="InvalidOperationException">
         /// Thrown if trying to get the value of a JSON token that is not a <see cref="JsonTokenType.String"/>.
index afc8ba0..efed281 100644 (file)
@@ -517,6 +517,9 @@ namespace System.Text.Json
                 case DateType.DateTimeOffset:
                     message = SR.FormatDateTimeOffset;
                     break;
+                case DateType.Base64String:
+                    message = SR.CannotDecodeInvalidBase64;
+                    break;
                 default:
                     Debug.Fail($"The DateType enum value: {dateType} is not part of the switch. Add the appropriate case and exception message.");
                     break;
@@ -580,6 +583,7 @@ namespace System.Text.Json
     internal enum DateType
     {
         DateTime,
-        DateTimeOffset
+        DateTimeOffset,
+        Base64String
     }
 }
index 891bd7e..fceb5f0 100644 (file)
@@ -20,7 +20,7 @@ namespace System.Text.Json
         private static ReadOnlySpan<byte> AllowList => new byte[byte.MaxValue + 1] {
             0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
             0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-            1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1,
+            1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0,
             1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1,
             1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
             1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1,
index 82b67a4..b9e64ca 100644 (file)
@@ -46,6 +46,13 @@ namespace System.Text.Json
         }
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static void ValidateBytes(ReadOnlySpan<byte> bytes)
+        {
+            if (bytes.Length > JsonConstants.MaxBase46ValueTokenSize)
+                ThrowHelper.ThrowArgumentException_ValueTooLarge(bytes.Length);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public static void ValidateDouble(double value)
         {
 #if BUILDING_INBOX_LIBRARY
@@ -113,6 +120,20 @@ namespace System.Text.Json
                 ThrowHelper.ThrowArgumentException(propertyName, value);
         }
 
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static void ValidatePropertyAndBytes(ReadOnlySpan<char> propertyName, ReadOnlySpan<byte> bytes)
+        {
+            if (propertyName.Length > JsonConstants.MaxCharacterTokenSize || bytes.Length > JsonConstants.MaxBase46ValueTokenSize)
+                ThrowHelper.ThrowArgumentException(propertyName, bytes);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static void ValidatePropertyAndBytes(ReadOnlySpan<byte> propertyName, ReadOnlySpan<byte> bytes)
+        {
+            if (propertyName.Length > JsonConstants.MaxTokenSize || bytes.Length > JsonConstants.MaxBase46ValueTokenSize)
+                ThrowHelper.ThrowArgumentException(propertyName, bytes);
+        }
+
         internal static void ValidateNumber(ReadOnlySpan<byte> utf8FormattedNumber)
         {
             // This is a simplified version of the number reader from Utf8JsonReader.TryGetNumber,
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Bytes.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Bytes.cs
new file mode 100644 (file)
index 0000000..dd4cf73
--- /dev/null
@@ -0,0 +1,376 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Buffers;
+using System.Buffers.Text;
+using System.Diagnostics;
+
+namespace System.Text.Json
+{
+    public sealed partial class Utf8JsonWriter
+    {
+        /// <summary>
+        /// Writes the pre-encoded property name and raw bytes value (as a base 64 encoded JSON string) as part of a name/value pair of a JSON object.
+        /// </summary>
+        /// <param name="propertyName">The JSON encoded property name of the JSON object to be transcoded and written as UTF-8.</param>
+        /// <param name="bytes">The binary data to be written as a base 64 encoded JSON string as part of the name/value pair.</param>
+        /// <remarks>
+        /// The property name should already be escaped when the instance of <see cref="JsonEncodedText"/> was created.
+        /// </remarks>
+        /// <exception cref="InvalidOperationException">
+        /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
+        /// </exception>
+        public void WriteBase64String(JsonEncodedText propertyName, ReadOnlySpan<byte> bytes)
+            => WriteBase64StringHelper(propertyName.EncodedUtf8Bytes, bytes);
+
+        private void WriteBase64StringHelper(ReadOnlySpan<byte> utf8PropertyName, ReadOnlySpan<byte> bytes)
+        {
+            Debug.Assert(utf8PropertyName.Length <= JsonConstants.MaxTokenSize);
+
+            JsonWriterHelper.ValidateBytes(bytes);
+
+            WriteBase64ByOptions(utf8PropertyName, bytes);
+
+            SetFlagToAddListSeparatorBeforeNextItem();
+            _tokenType = JsonTokenType.String;
+        }
+
+        /// <summary>
+        /// Writes the property name and raw bytes value (as a Base64 encoded JSON string) as part of a name/value pair of a JSON object.
+        /// </summary>
+        /// <param name="propertyName">The property name of the JSON object to be transcoded and written as UTF-8.</param>
+        /// <param name="bytes">The binary data to be written as a base 64 encoded JSON string as part of the name/value pair.</param>
+        /// <remarks>
+        /// The property name is escaped before writing.
+        /// </remarks>
+        /// <exception cref="ArgumentException">
+        /// Thrown when the specified property name is too large.
+        /// </exception>
+        /// <exception cref="InvalidOperationException">
+        /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
+        /// </exception>
+        public void WriteBase64String(string propertyName, ReadOnlySpan<byte> bytes)
+            => WriteBase64String(propertyName.AsSpan(), bytes);
+
+        /// <summary>
+        /// Writes the property name and raw bytes value (as a base 64 encoded JSON string) as part of a name/value pair of a JSON object.
+        /// </summary>
+        /// <param name="propertyName">The property name of the JSON object to be transcoded and written as UTF-8.</param>
+        /// <param name="bytes">The binary data to be written as a base 64 encoded JSON string as part of the name/value pair.</param>
+        /// <remarks>
+        /// The property name is escaped before writing.
+        /// </remarks>
+        /// <exception cref="ArgumentException">
+        /// Thrown when the specified property name is too large.
+        /// </exception>
+        /// <exception cref="InvalidOperationException">
+        /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
+        /// </exception>
+        public void WriteBase64String(ReadOnlySpan<char> propertyName, ReadOnlySpan<byte> bytes)
+        {
+            JsonWriterHelper.ValidatePropertyAndBytes(propertyName, bytes);
+
+            WriteBase64Escape(propertyName, bytes);
+
+            SetFlagToAddListSeparatorBeforeNextItem();
+            _tokenType = JsonTokenType.String;
+        }
+
+        /// <summary>
+        /// Writes the property name and raw bytes value (as a base 64 encoded JSON string) as part of a name/value pair of a JSON object.
+        /// </summary>
+        /// <param name="utf8PropertyName">The UTF-8 encoded property name of the JSON object to be written.</param>
+        /// <param name="bytes">The binary data to be written as a base 64 encoded JSON string as part of the name/value pair.</param>
+        /// <remarks>
+        /// The property name is escaped before writing.
+        /// </remarks>
+        /// <exception cref="ArgumentException">
+        /// Thrown when the specified property name is too large.
+        /// </exception>
+        /// <exception cref="InvalidOperationException">
+        /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
+        /// </exception>
+        public void WriteBase64String(ReadOnlySpan<byte> utf8PropertyName, ReadOnlySpan<byte> bytes)
+        {
+            JsonWriterHelper.ValidatePropertyAndBytes(utf8PropertyName, bytes);
+
+            WriteBase64Escape(utf8PropertyName, bytes);
+
+            SetFlagToAddListSeparatorBeforeNextItem();
+            _tokenType = JsonTokenType.String;
+        }
+
+        private void WriteBase64Escape(ReadOnlySpan<char> propertyName, ReadOnlySpan<byte> bytes)
+        {
+            int propertyIdx = JsonWriterHelper.NeedsEscaping(propertyName);
+
+            Debug.Assert(propertyIdx >= -1 && propertyIdx < propertyName.Length);
+
+            if (propertyIdx != -1)
+            {
+                WriteBase64EscapeProperty(propertyName, bytes, propertyIdx);
+            }
+            else
+            {
+                WriteBase64ByOptions(propertyName, bytes);
+            }
+        }
+
+        private void WriteBase64Escape(ReadOnlySpan<byte> utf8PropertyName, ReadOnlySpan<byte> bytes)
+        {
+            int propertyIdx = JsonWriterHelper.NeedsEscaping(utf8PropertyName);
+
+            Debug.Assert(propertyIdx >= -1 && propertyIdx < utf8PropertyName.Length);
+
+            if (propertyIdx != -1)
+            {
+                WriteBase64EscapeProperty(utf8PropertyName, bytes, propertyIdx);
+            }
+            else
+            {
+                WriteBase64ByOptions(utf8PropertyName, bytes);
+            }
+        }
+
+        private void WriteBase64EscapeProperty(ReadOnlySpan<char> propertyName, ReadOnlySpan<byte> bytes, int firstEscapeIndexProp)
+        {
+            Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= propertyName.Length);
+            Debug.Assert(firstEscapeIndexProp >= 0 && firstEscapeIndexProp < propertyName.Length);
+
+            char[] propertyArray = null;
+
+            int length = JsonWriterHelper.GetMaxEscapedLength(propertyName.Length, firstEscapeIndexProp);
+
+            Span<char> escapedPropertyName = length <= JsonConstants.StackallocThreshold ?
+                stackalloc char[length] :
+                (propertyArray = ArrayPool<char>.Shared.Rent(length));
+
+            JsonWriterHelper.EscapeString(propertyName, escapedPropertyName, firstEscapeIndexProp, out int written);
+
+            WriteBase64ByOptions(escapedPropertyName.Slice(0, written), bytes);
+
+            if (propertyArray != null)
+            {
+                ArrayPool<char>.Shared.Return(propertyArray);
+            }
+        }
+
+        private void WriteBase64EscapeProperty(ReadOnlySpan<byte> utf8PropertyName, ReadOnlySpan<byte> bytes, int firstEscapeIndexProp)
+        {
+            Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= utf8PropertyName.Length);
+            Debug.Assert(firstEscapeIndexProp >= 0 && firstEscapeIndexProp < utf8PropertyName.Length);
+
+            byte[] propertyArray = null;
+
+            int length = JsonWriterHelper.GetMaxEscapedLength(utf8PropertyName.Length, firstEscapeIndexProp);
+
+            Span<byte> escapedPropertyName = length <= JsonConstants.StackallocThreshold ?
+                stackalloc byte[length] :
+                (propertyArray = ArrayPool<byte>.Shared.Rent(length));
+
+            JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, out int written);
+
+            WriteBase64ByOptions(escapedPropertyName.Slice(0, written), bytes);
+
+            if (propertyArray != null)
+            {
+                ArrayPool<byte>.Shared.Return(propertyArray);
+            }
+        }
+
+        private void WriteBase64ByOptions(ReadOnlySpan<char> propertyName, ReadOnlySpan<byte> bytes)
+        {
+            ValidateWritingProperty();
+            if (Options.Indented)
+            {
+                WriteBase64Indented(propertyName, bytes);
+            }
+            else
+            {
+                WriteBase64Minimized(propertyName, bytes);
+            }
+        }
+
+        private void WriteBase64ByOptions(ReadOnlySpan<byte> utf8PropertyName, ReadOnlySpan<byte> bytes)
+        {
+            ValidateWritingProperty();
+            if (Options.Indented)
+            {
+                WriteBase64Indented(utf8PropertyName, bytes);
+            }
+            else
+            {
+                WriteBase64Minimized(utf8PropertyName, bytes);
+            }
+        }
+
+        private void WriteBase64Minimized(ReadOnlySpan<char> escapedPropertyName, ReadOnlySpan<byte> bytes)
+        {
+            int encodedLength = Base64.GetMaxEncodedToUtf8Length(bytes.Length);
+
+            Debug.Assert(escapedPropertyName.Length * JsonConstants.MaxExpansionFactorWhileTranscoding < int.MaxValue - (encodedLength * JsonConstants.MaxExpansionFactorWhileEscaping) - 6);
+
+            // All ASCII, 2 quotes for property name, 2 quotes to surround the base-64 encoded string value, and 1 colon => escapedPropertyName.Length + encodedLength + 5
+            // Optionally, 1 list separator, and up to 3x growth when transcoding, with escaping which can by up to 6x.
+            int maxRequired = (escapedPropertyName.Length * JsonConstants.MaxExpansionFactorWhileTranscoding) + (encodedLength * JsonConstants.MaxExpansionFactorWhileEscaping) + 6;
+
+            if (_memory.Length - BytesPending < maxRequired)
+            {
+                Grow(maxRequired);
+            }
+
+            Span<byte> output = _memory.Span;
+
+            if (_currentDepth < 0)
+            {
+                output[BytesPending++] = JsonConstants.ListSeparator;
+            }
+            output[BytesPending++] = JsonConstants.Quote;
+
+            TranscodeAndWrite(escapedPropertyName, output);
+
+            output[BytesPending++] = JsonConstants.Quote;
+            output[BytesPending++] = JsonConstants.KeyValueSeperator;
+
+            output[BytesPending++] = JsonConstants.Quote;
+
+            Base64EncodeAndWrite(bytes, output, encodedLength);
+
+            output[BytesPending++] = JsonConstants.Quote;
+        }
+
+        private void WriteBase64Minimized(ReadOnlySpan<byte> escapedPropertyName, ReadOnlySpan<byte> bytes)
+        {
+            int encodedLength = Base64.GetMaxEncodedToUtf8Length(bytes.Length);
+
+            Debug.Assert(escapedPropertyName.Length < int.MaxValue - (encodedLength * JsonConstants.MaxExpansionFactorWhileEscaping) - 6);
+
+            // 2 quotes for property name, 2 quotes to surround the base-64 encoded string value, and 1 colon => escapedPropertyName.Length + encodedLength + 5
+            // Optionally, 1 list separator, with escaping which can by up to 6x.
+            int maxRequired = escapedPropertyName.Length + (encodedLength * JsonConstants.MaxExpansionFactorWhileEscaping) + 6;
+
+            if (_memory.Length - BytesPending < maxRequired)
+            {
+                Grow(maxRequired);
+            }
+
+            Span<byte> output = _memory.Span;
+
+            if (_currentDepth < 0)
+            {
+                output[BytesPending++] = JsonConstants.ListSeparator;
+            }
+            output[BytesPending++] = JsonConstants.Quote;
+
+            escapedPropertyName.CopyTo(output.Slice(BytesPending));
+            BytesPending += escapedPropertyName.Length;
+
+            output[BytesPending++] = JsonConstants.Quote;
+            output[BytesPending++] = JsonConstants.KeyValueSeperator;
+
+            output[BytesPending++] = JsonConstants.Quote;
+
+            Base64EncodeAndWrite(bytes, output, encodedLength);
+
+            output[BytesPending++] = JsonConstants.Quote;
+        }
+
+        private void WriteBase64Indented(ReadOnlySpan<char> escapedPropertyName, ReadOnlySpan<byte> bytes)
+        {
+            int indent = Indentation;
+            Debug.Assert(indent <= 2 * JsonConstants.MaxWriterDepth);
+
+            int encodedLength = Base64.GetMaxEncodedToUtf8Length(bytes.Length);
+
+            Debug.Assert(escapedPropertyName.Length * JsonConstants.MaxExpansionFactorWhileTranscoding < int.MaxValue - indent - (encodedLength * JsonConstants.MaxExpansionFactorWhileEscaping) - 7 - s_newLineLength);
+
+            // All ASCII, 2 quotes for property name, 2 quotes to surround the base-64 encoded string value, 1 colon, and 1 space => indent + escapedPropertyName.Length + encodedLength + 6
+            // Optionally, 1 list separator, 1-2 bytes for new line, and up to 3x growth when transcoding, with escaping which can by up to 6x.
+            int maxRequired = indent + (escapedPropertyName.Length * JsonConstants.MaxExpansionFactorWhileTranscoding) + (encodedLength * JsonConstants.MaxExpansionFactorWhileEscaping) + 7 + s_newLineLength;
+
+            if (_memory.Length - BytesPending < maxRequired)
+            {
+                Grow(maxRequired);
+            }
+
+            Span<byte> output = _memory.Span;
+
+            if (_currentDepth < 0)
+            {
+                output[BytesPending++] = JsonConstants.ListSeparator;
+            }
+
+            if (_tokenType != JsonTokenType.None)
+            {
+                WriteNewLine(output);
+            }
+
+            JsonWriterHelper.WriteIndentation(output.Slice(BytesPending), indent);
+            BytesPending += indent;
+
+            output[BytesPending++] = JsonConstants.Quote;
+
+            TranscodeAndWrite(escapedPropertyName, output);
+
+            output[BytesPending++] = JsonConstants.Quote;
+            output[BytesPending++] = JsonConstants.KeyValueSeperator;
+            output[BytesPending++] = JsonConstants.Space;
+
+            output[BytesPending++] = JsonConstants.Quote;
+
+            Base64EncodeAndWrite(bytes, output, encodedLength);
+
+            output[BytesPending++] = JsonConstants.Quote;
+        }
+
+        private void WriteBase64Indented(ReadOnlySpan<byte> escapedPropertyName, ReadOnlySpan<byte> bytes)
+        {
+            int indent = Indentation;
+            Debug.Assert(indent <= 2 * JsonConstants.MaxWriterDepth);
+
+            int encodedLength = Base64.GetMaxEncodedToUtf8Length(bytes.Length);
+
+            Debug.Assert(escapedPropertyName.Length < int.MaxValue - indent - (encodedLength * JsonConstants.MaxExpansionFactorWhileEscaping) - 7 - s_newLineLength);
+
+            // 2 quotes for property name, 2 quotes to surround the base-64 encoded string value, 1 colon, and 1 space => indent + escapedPropertyName.Length + encodedLength + 6
+            // Optionally, 1 list separator, and 1-2 bytes for new line, with escaping which can by up to 6x.
+            int maxRequired = indent + escapedPropertyName.Length + (encodedLength * JsonConstants.MaxExpansionFactorWhileEscaping) + 7 + s_newLineLength;
+
+            if (_memory.Length - BytesPending < maxRequired)
+            {
+                Grow(maxRequired);
+            }
+
+            Span<byte> output = _memory.Span;
+
+            if (_currentDepth < 0)
+            {
+                output[BytesPending++] = JsonConstants.ListSeparator;
+            }
+
+            if (_tokenType != JsonTokenType.None)
+            {
+                WriteNewLine(output);
+            }
+
+            JsonWriterHelper.WriteIndentation(output.Slice(BytesPending), indent);
+            BytesPending += indent;
+
+            output[BytesPending++] = JsonConstants.Quote;
+
+            escapedPropertyName.CopyTo(output.Slice(BytesPending));
+            BytesPending += escapedPropertyName.Length;
+
+            output[BytesPending++] = JsonConstants.Quote;
+            output[BytesPending++] = JsonConstants.KeyValueSeperator;
+            output[BytesPending++] = JsonConstants.Space;
+
+            output[BytesPending++] = JsonConstants.Quote;
+
+            Base64EncodeAndWrite(bytes, output, encodedLength);
+
+            output[BytesPending++] = JsonConstants.Quote;
+        }
+    }
+}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Bytes.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Bytes.cs
new file mode 100644 (file)
index 0000000..ef1fdb8
--- /dev/null
@@ -0,0 +1,119 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Buffers.Text;
+using System.Diagnostics;
+
+namespace System.Text.Json
+{
+    public sealed partial class Utf8JsonWriter
+    {
+        /// <summary>
+        /// Writes the raw bytes value as base 64 encoded JSON string as an element of a JSON array.
+        /// </summary>
+        /// <param name="bytes">The binary data to be written as a base 64 encoded JSON string element of a JSON array.</param>
+        /// <remarks>
+        /// The bytes are encoded before writing.
+        /// </remarks>
+        /// <exception cref="ArgumentException">
+        /// Thrown when the specified value is too large.
+        /// </exception>
+        /// <exception cref="InvalidOperationException">
+        /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
+        /// </exception>
+        public void WriteBase64StringValue(ReadOnlySpan<byte> bytes)
+        {
+            JsonWriterHelper.ValidateBytes(bytes);
+
+            WriteBase64ByOptions(bytes);
+
+            SetFlagToAddListSeparatorBeforeNextItem();
+            _tokenType = JsonTokenType.String;
+        }
+
+        private void WriteBase64ByOptions(ReadOnlySpan<byte> bytes)
+        {
+            ValidateWritingValue();
+
+            if (Options.Indented)
+            {
+                WriteBase64Indented(bytes);
+            }
+            else
+            {
+                WriteBase64Minimized(bytes);
+            }
+        }
+
+        // TODO: https://github.com/dotnet/corefx/issues/36958
+        private void WriteBase64Minimized(ReadOnlySpan<byte> bytes)
+        {
+            int encodingLength = Base64.GetMaxEncodedToUtf8Length(bytes.Length);
+
+            Debug.Assert(encodingLength < (int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping) - 3);
+
+            // 2 quotes to surround the base-64 encoded string value, with escaping which can by up to 6x.
+            // Optionally, 1 list separator
+            int maxRequired = (encodingLength * JsonConstants.MaxExpansionFactorWhileEscaping) + 3;
+
+            if (_memory.Length - BytesPending < maxRequired)
+            {
+                Grow(maxRequired);
+            }
+
+            Span<byte> output = _memory.Span;
+
+            if (_currentDepth < 0)
+            {
+                output[BytesPending++] = JsonConstants.ListSeparator;
+            }
+            output[BytesPending++] = JsonConstants.Quote;
+
+            Base64EncodeAndWrite(bytes, output, encodingLength);
+
+            output[BytesPending++] = JsonConstants.Quote;
+        }
+
+        // TODO: https://github.com/dotnet/corefx/issues/36958
+        private void WriteBase64Indented(ReadOnlySpan<byte> bytes)
+        {
+            int indent = Indentation;
+            Debug.Assert(indent <= 2 * JsonConstants.MaxWriterDepth);
+
+            int encodingLength = Base64.GetMaxEncodedToUtf8Length(bytes.Length);
+
+            Debug.Assert(encodingLength < (int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping) - indent - 3 - s_newLineLength);
+
+            // indentation + 2 quotes to surround the base-64 encoded string value, with escaping which can by up to 6x.
+            // Optionally, 1 list separator, and 1-2 bytes for new line
+            int maxRequired = indent + (encodingLength * JsonConstants.MaxExpansionFactorWhileEscaping) + 3 + s_newLineLength;
+
+            if (_memory.Length - BytesPending < maxRequired)
+            {
+                Grow(maxRequired);
+            }
+
+            Span<byte> output = _memory.Span;
+
+            if (_currentDepth < 0)
+            {
+                output[BytesPending++] = JsonConstants.ListSeparator;
+            }
+
+            if (_tokenType != JsonTokenType.None)
+            {
+                WriteNewLine(output);
+            }
+
+            JsonWriterHelper.WriteIndentation(output.Slice(BytesPending), indent);
+            BytesPending += indent;
+
+            output[BytesPending++] = JsonConstants.Quote;
+
+            Base64EncodeAndWrite(bytes, output, encodingLength);
+
+            output[BytesPending++] = JsonConstants.Quote;
+        }
+    }
+}
index cc4986e..9c26cb3 100644 (file)
@@ -2,7 +2,10 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 // See the LICENSE file in the project root for more information.
 
+using System.Buffers;
+using System.Buffers.Text;
 using System.Diagnostics;
+using System.Runtime.CompilerServices;
 
 namespace System.Text.Json
 {
@@ -26,5 +29,41 @@ namespace System.Text.Json
                 }
             }
         }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private void Base64EncodeAndWrite(ReadOnlySpan<byte> bytes, Span<byte> output, int encodingLength)
+        {
+            byte[] outputText = null;
+
+            Span<byte> encodedBytes = encodingLength <= JsonConstants.StackallocThreshold ?
+                stackalloc byte[encodingLength] :
+                (outputText = ArrayPool<byte>.Shared.Rent(encodingLength));
+
+            OperationStatus status = Base64.EncodeToUtf8(bytes, encodedBytes, out int consumed, out int written);
+            Debug.Assert(status == OperationStatus.Done);
+            Debug.Assert(consumed == bytes.Length);
+
+            encodedBytes = encodedBytes.Slice(0, written);
+            Span<byte> destination = output.Slice(BytesPending);
+
+            int firstEscapeIndexVal = encodedBytes.IndexOfAny(JsonConstants.Plus, JsonConstants.Slash);
+            if (firstEscapeIndexVal == -1)
+            {
+                Debug.Assert(destination.Length >= written);
+                encodedBytes.Slice(0, written).CopyTo(destination);
+                BytesPending += written;
+            }
+            else
+            {
+                Debug.Assert(destination.Length >= written * JsonConstants.MaxExpansionFactorWhileEscaping);
+                JsonWriterHelper.EscapeString(encodedBytes, destination, firstEscapeIndexVal, out written);
+                BytesPending += written;
+            }
+
+            if (outputText != null)
+            {
+                ArrayPool<byte>.Shared.Return(outputText);
+            }
+        }
     }
 }
index 36abf6b..c8d74f3 100644 (file)
@@ -371,7 +371,7 @@ namespace System.Text.Json.Tests
                 null,
                 bytes => JsonDocument.Parse(new MemoryStream(Utf8Bom.Concat(bytes).ToArray())));
         }
-        
+
         [Theory]
         [MemberData(nameof(ReducedTestCases))]
         public static void ParseJson_SeekableStream_Async_WithBOM(bool compactData, TestCaseType type, string jsonString)
@@ -540,12 +540,12 @@ namespace System.Text.Json.Tests
             }
         }
 
-        private static string PrintJson(this JsonDocument document, int sizeHint=0)
+        private static string PrintJson(this JsonDocument document, int sizeHint = 0)
         {
             return PrintJson(document.RootElement, sizeHint);
         }
 
-        private static string PrintJson(this JsonElement element, int sizeHint=0)
+        private static string PrintJson(this JsonElement element, int sizeHint = 0)
         {
             StringBuilder sb = new StringBuilder(sizeHint);
             DepthFirstAppend(sb, element);
@@ -562,31 +562,31 @@ namespace System.Text.Json.Tests
                 case JsonValueType.True:
                 case JsonValueType.String:
                 case JsonValueType.Number:
-                {
-                    buf.Append(element.ToString());
-                    buf.Append(", ");
-                    break;
-                }
-                case JsonValueType.Object:
-                {
-                    foreach (JsonProperty prop in element.EnumerateObject())
                     {
-                        buf.Append(prop.Name);
+                        buf.Append(element.ToString());
                         buf.Append(", ");
-                        DepthFirstAppend(buf, prop.Value);
+                        break;
                     }
+                case JsonValueType.Object:
+                    {
+                        foreach (JsonProperty prop in element.EnumerateObject())
+                        {
+                            buf.Append(prop.Name);
+                            buf.Append(", ");
+                            DepthFirstAppend(buf, prop.Value);
+                        }
 
-                    break;
-                }
+                        break;
+                    }
                 case JsonValueType.Array:
-                {
-                    foreach (JsonElement child in element.EnumerateArray())
                     {
-                        DepthFirstAppend(buf, child);
-                    }
+                        foreach (JsonElement child in element.EnumerateArray())
+                        {
+                            DepthFirstAppend(buf, child);
+                        }
 
-                    break;
-                }
+                        break;
+                    }
             }
         }
 
@@ -834,6 +834,8 @@ namespace System.Text.Json.Tests
                 }
 
                 Assert.Throws<InvalidOperationException>(() => root.GetString());
+                Assert.Throws<InvalidOperationException>(() => root.GetBytesFromBase64());
+                Assert.Throws<InvalidOperationException>(() => root.TryGetBytesFromBase64(out byte[] bytes));
                 Assert.Throws<InvalidOperationException>(() => root.GetDateTime());
                 Assert.Throws<InvalidOperationException>(() => root.GetDateTimeOffset());
                 Assert.Throws<InvalidOperationException>(() => root.GetGuid());
@@ -920,6 +922,7 @@ namespace System.Text.Json.Tests
                 }
 
                 Assert.Throws<InvalidOperationException>(() => root.GetString());
+                Assert.Throws<InvalidOperationException>(() => root.GetBytesFromBase64());
                 Assert.Throws<InvalidOperationException>(() => root.GetDateTime());
                 Assert.Throws<InvalidOperationException>(() => root.GetDateTimeOffset());
                 Assert.Throws<InvalidOperationException>(() => root.GetGuid());
@@ -976,6 +979,7 @@ namespace System.Text.Json.Tests
                 Assert.Equal(value, root.GetUInt64());
 
                 Assert.Throws<InvalidOperationException>(() => root.GetString());
+                Assert.Throws<InvalidOperationException>(() => root.GetBytesFromBase64());
                 Assert.Throws<InvalidOperationException>(() => root.GetDateTime());
                 Assert.Throws<InvalidOperationException>(() => root.GetDateTimeOffset());
                 Assert.Throws<InvalidOperationException>(() => root.GetGuid());
@@ -1032,6 +1036,7 @@ namespace System.Text.Json.Tests
                 Assert.Throws<FormatException>(() => root.GetUInt64());
 
                 Assert.Throws<InvalidOperationException>(() => root.GetString());
+                Assert.Throws<InvalidOperationException>(() => root.GetBytesFromBase64());
                 Assert.Throws<InvalidOperationException>(() => root.GetDateTime());
                 Assert.Throws<InvalidOperationException>(() => root.GetDateTimeOffset());
                 Assert.Throws<InvalidOperationException>(() => root.GetGuid());
@@ -1220,6 +1225,7 @@ namespace System.Text.Json.Tests
                 Assert.Throws<FormatException>(() => root.GetUInt64());
 
                 Assert.Throws<InvalidOperationException>(() => root.GetString());
+                Assert.Throws<InvalidOperationException>(() => root.GetBytesFromBase64());
                 Assert.Throws<InvalidOperationException>(() => root.GetDateTime());
                 Assert.Throws<InvalidOperationException>(() => root.GetDateTimeOffset());
                 Assert.Throws<InvalidOperationException>(() => root.GetGuid());
@@ -1287,6 +1293,7 @@ namespace System.Text.Json.Tests
                 Assert.Throws<FormatException>(() => root.GetUInt64());
 
                 Assert.Throws<InvalidOperationException>(() => root.GetString());
+                Assert.Throws<InvalidOperationException>(() => root.GetBytesFromBase64());
                 Assert.Throws<InvalidOperationException>(() => root.GetDateTime());
                 Assert.Throws<InvalidOperationException>(() => root.GetDateTimeOffset());
                 Assert.Throws<InvalidOperationException>(() => root.GetGuid());
@@ -1338,6 +1345,8 @@ namespace System.Text.Json.Tests
                 Assert.Throws<InvalidOperationException>(() => root.GetUInt64());
                 Assert.Throws<InvalidOperationException>(() => root.TryGetUInt64(out ulong _));
                 Assert.Throws<InvalidOperationException>(() => root.GetString());
+                Assert.Throws<InvalidOperationException>(() => root.GetBytesFromBase64());
+                Assert.Throws<InvalidOperationException>(() => root.TryGetBytesFromBase64(out byte[] _));
                 Assert.Throws<InvalidOperationException>(() => root.GetDateTime());
                 Assert.Throws<InvalidOperationException>(() => root.GetDateTimeOffset());
                 Assert.Throws<InvalidOperationException>(() => root.GetGuid());
@@ -1367,6 +1376,8 @@ namespace System.Text.Json.Tests
                 Assert.Throws<ObjectDisposedException>(() => root.GetUInt64());
                 Assert.Throws<ObjectDisposedException>(() => root.TryGetUInt64(out ulong _));
                 Assert.Throws<ObjectDisposedException>(() => root.GetString());
+                Assert.Throws<ObjectDisposedException>(() => root.GetBytesFromBase64());
+                Assert.Throws<ObjectDisposedException>(() => root.TryGetBytesFromBase64(out byte[] _));
                 Assert.Throws<ObjectDisposedException>(() => root.GetBoolean());
                 Assert.Throws<ObjectDisposedException>(() => root.GetRawText());
 
@@ -1409,6 +1420,8 @@ namespace System.Text.Json.Tests
             Assert.Throws<InvalidOperationException>(() => root.GetUInt64());
             Assert.Throws<InvalidOperationException>(() => root.TryGetUInt64(out ulong _));
             Assert.Throws<InvalidOperationException>(() => root.GetString());
+            Assert.Throws<InvalidOperationException>(() => root.GetBytesFromBase64());
+            Assert.Throws<InvalidOperationException>(() => root.TryGetBytesFromBase64(out byte[] _));
             Assert.Throws<InvalidOperationException>(() => root.GetDateTime());
             Assert.Throws<InvalidOperationException>(() => root.GetDateTimeOffset());
             Assert.Throws<InvalidOperationException>(() => root.GetGuid());
@@ -1467,6 +1480,107 @@ namespace System.Text.Json.Tests
             }
         }
 
+        [Fact]
+        public static void GetBase64String_BadUtf8()
+        {
+            // The Arabic ligature Lam-Alef (U+FEFB) (which happens to, as a standalone, mean "no" in English)
+            // is UTF-8 EF BB BB.  So let's leave out a BB and put it in quotes.
+            using (JsonDocument doc = JsonDocument.Parse(new byte[] { 0x22, 0xEF, 0xBB, 0x22 }))
+            {
+                JsonElement root = doc.RootElement;
+
+                Assert.Equal(JsonValueType.String, root.Type);
+                Assert.Throws<FormatException>(() => root.GetBytesFromBase64());
+                Assert.False(root.TryGetBytesFromBase64(out byte[] value));
+                Assert.Null(value);
+            }
+        }
+
+        [Fact]
+        public static void GetBase64Unescapes()
+        {
+            string jsonString = "\"\\u0031234\""; // equivalent to "\"1234\""
+
+            byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
+
+            using (JsonDocument doc = JsonDocument.Parse(dataUtf8))
+            {
+                byte[] expected = Convert.FromBase64String("1234"); // new byte[3] { 215, 109, 248 }
+
+                Assert.Equal(expected, doc.RootElement.GetBytesFromBase64());
+
+                Assert.True(doc.RootElement.TryGetBytesFromBase64(out byte[] value));
+                Assert.Equal(expected, value);
+            }
+        }
+
+        [Theory]
+        [InlineData("\"ABC=\"")]
+        [InlineData("\"AB+D\"")]
+        [InlineData("\"ABCD\"")]
+        [InlineData("\"ABC/\"")]
+        [InlineData("\"++++\"")]
+        [InlineData(null)]  // Large randomly generated string
+        public static void ReadBase64String(string jsonString)
+        {
+            if (jsonString == null)
+            {
+                var random = new Random(42);
+                var charArray = new char[502];
+                charArray[0] = '"';
+                for (int i = 1; i < charArray.Length; i++)
+                {
+                    charArray[i] = (char)random.Next('A', 'Z'); // ASCII values (between 65 and 90) that constitute valid base 64 string.
+                }
+                charArray[charArray.Length - 1] = '"';
+                jsonString = new string(charArray);
+            }
+
+            byte[] expected = Convert.FromBase64String(jsonString.AsSpan(1, jsonString.Length - 2).ToString());
+
+            using (JsonDocument doc = JsonDocument.Parse(jsonString))
+            {
+                Assert.Equal(expected, doc.RootElement.GetBytesFromBase64());
+
+                Assert.True(doc.RootElement.TryGetBytesFromBase64(out byte[] value));
+                Assert.Equal(expected, value);
+            }
+        }
+
+        [Theory]
+        [InlineData("\"ABC===\"")]
+        [InlineData("\"ABC\"")]
+        [InlineData("\"ABC!\"")]
+        [InlineData(null)]  // Large randomly generated string
+        public static void InvalidBase64(string jsonString)
+        {
+            if (jsonString == null)
+            {
+                var random = new Random(42);
+                var charArray = new char[500];
+                charArray[0] = '"';
+                for (int i = 1; i < charArray.Length; i++)
+                {
+                    charArray[i] = (char)random.Next('?', '\\'); // ASCII values (between 63 and 91) that don't need to be escaped.
+                }
+
+                charArray[256] = '\\';
+                charArray[257] = '"';
+                charArray[charArray.Length - 1] = '"';
+                jsonString = new string(charArray);
+            }
+
+            byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
+
+            using (JsonDocument doc = JsonDocument.Parse(dataUtf8))
+            {
+                Assert.False(doc.RootElement.TryGetBytesFromBase64(out byte[] value));
+                Assert.Null(value);
+
+                Assert.Throws<FormatException>(() => doc.RootElement.GetBytesFromBase64());
+            }
+        }
+
         [Theory]
         [InlineData(" { \"hi\": \"there\" }")]
         [InlineData(" { \n\n\n\n } ")]
@@ -1878,8 +1992,8 @@ namespace System.Text.Json.Tests
         public static void GetRawText()
         {
             const string json =
-                // Don't let there be a newline before the first embedded quote,
-                // because the index would change across CRLF vs LF compile environments.
+// Don't let there be a newline before the first embedded quote,
+// because the index would change across CRLF vs LF compile environments.
 @"{  ""  weird  property  name""
                   :
        {
@@ -2925,7 +3039,7 @@ namespace System.Text.Json.Tests
             ReadOnlyMemory<byte> data,
             int segmentCount,
             in JsonReaderState state,
-            bool isFinalBlock=false)
+            bool isFinalBlock = false)
         {
             if (segmentCount == 0)
             {
index 642cb87..6468872 100644 (file)
@@ -495,6 +495,22 @@ namespace System.Text.Json.Tests
 
                     try
                     {
+                        byte[] value = json.GetBytesFromBase64();
+                        Assert.True(false, "Expected GetBytesFromBase64 to throw InvalidOperationException due to mismatch token type.");
+                    }
+                    catch (InvalidOperationException)
+                    { }
+
+                    try
+                    {
+                        json.TryGetBytesFromBase64(out byte[] value);
+                        Assert.True(false, "Expected TryGetBytesFromBase64 to throw InvalidOperationException due to mismatch token type.");
+                    }
+                    catch (InvalidOperationException)
+                    { }
+
+                    try
+                    {
                         DateTime value = json.GetDateTime();
                         Assert.True(false, "Expected GetDateTime to throw InvalidOperationException due to mismatched token type.");
                     }
@@ -813,6 +829,170 @@ namespace System.Text.Json.Tests
             }
         }
 
+        [Fact]
+        public static void GetBase64Unescapes()
+        {
+            string jsonString = "\"\\u0031234\""; // equivalent to "\"1234\""
+
+            byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
+
+            var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default);
+            Assert.True(json.Read());
+
+            byte[] expected = Convert.FromBase64String("1234"); // new byte[3] { 215, 109, 248 }
+
+            byte[] value = json.GetBytesFromBase64();
+            Assert.Equal(expected, value);
+            Assert.True(json.TryGetBytesFromBase64(out value));
+            Assert.Equal(expected, value);
+        }
+
+        [Theory]
+        [InlineData("\"ABC=\"")]
+        [InlineData("\"AB+D\"")]
+        [InlineData("\"ABCD\"")]
+        [InlineData("\"ABC/\"")]
+        [InlineData("\"++++\"")]
+        [InlineData(null)]  // Large randomly generated string
+        public static void ValidBase64(string jsonString)
+        {
+            if (jsonString == null)
+            {
+                var random = new Random(42);
+                var charArray = new char[502];
+                charArray[0] = '"';
+                for (int i = 1; i < charArray.Length; i++)
+                {
+                    charArray[i] = (char)random.Next('A', 'Z'); // ASCII values (between 65 and 90) that constitute valid base 64 string.
+                }
+                charArray[charArray.Length - 1] = '"';
+                jsonString = new string(charArray);
+            }
+
+            byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
+
+            var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default);
+            Assert.True(json.Read());
+
+            byte[] expected = Convert.FromBase64String(jsonString.AsSpan(1, jsonString.Length - 2).ToString());
+
+            byte[] value = json.GetBytesFromBase64();
+            Assert.Equal(expected, value);
+            Assert.True(json.TryGetBytesFromBase64(out value));
+            Assert.Equal(expected, value);
+        }
+
+        [Theory]
+        [InlineData("\"ABC===\"")]
+        [InlineData("\"ABC\"")]
+        [InlineData("\"ABC!\"")]
+        [InlineData(null)]  // Large randomly generated string
+        public static void InvalidBase64(string jsonString)
+        {
+            if (jsonString == null)
+            {
+                var random = new Random(42);
+                var charArray = new char[500];
+                charArray[0] = '"';
+                for (int i = 1; i < charArray.Length; i++)
+                {
+                    charArray[i] = (char)random.Next('?', '\\'); // ASCII values (between 63 and 91) that don't need to be escaped.
+                }
+
+                charArray[256] = '\\';
+                charArray[257] = '"';
+                charArray[charArray.Length - 1] = '"';
+                jsonString = new string(charArray);
+            }
+
+            byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
+
+            var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default);
+            Assert.True(json.Read());
+            Assert.False(json.TryGetBytesFromBase64(out byte[] value));
+            Assert.Null(value);
+
+            try
+            {
+                byte[] val = json.GetBytesFromBase64();
+                Assert.True(false, "Expected InvalidOperationException when trying to decode base 64 string for invalid UTF-16 JSON text.");
+            }
+            catch (FormatException) { }
+        }
+
+        [Theory]
+        [InlineData("\"a\\uDD1E\"")]
+        [InlineData("\"a\\uDD1Eb\"")]
+        [InlineData("\"a\\uD834\"")]
+        [InlineData("\"a\\uD834\\u0030\"")]
+        [InlineData("\"a\\uD834\\uD834\"")]
+        [InlineData("\"a\\uD834b\"")]
+        [InlineData("\"a\\uDD1E\\uD834b\"")]
+        [InlineData("\"a\\\\uD834\\uDD1Eb\"")]
+        [InlineData("\"a\\uDD1E\\\\uD834b\"")]
+        public static void TestingGetBase64InvalidUTF16(string jsonString)
+        {
+            byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
+
+            foreach (JsonCommentHandling commentHandling in Enum.GetValues(typeof(JsonCommentHandling)))
+            {
+                var state = new JsonReaderState(options: new JsonReaderOptions { CommentHandling = commentHandling });
+                var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state);
+
+                Assert.True(json.Read());
+                Assert.Equal(JsonTokenType.String, json.TokenType);
+                try
+                {
+                    byte[] val = json.GetBytesFromBase64();
+                    Assert.True(false, "Expected InvalidOperationException when trying to decode base 64 string for invalid UTF-16 JSON text.");
+                }
+                catch (InvalidOperationException) { }
+
+                try
+                {
+                    json.TryGetBytesFromBase64(out byte[] val);
+                    Assert.True(false, "Expected InvalidOperationException when trying to decode base 64 string for invalid UTF-16 JSON text.");
+                }
+                catch (InvalidOperationException) { }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(InvalidUTF8Strings))]
+        public static void TestingGetBase64InvalidUTF8(byte[] dataUtf8)
+        {
+            foreach (JsonCommentHandling commentHandling in Enum.GetValues(typeof(JsonCommentHandling)))
+            {
+                var state = new JsonReaderState(options: new JsonReaderOptions { CommentHandling = commentHandling });
+                var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state);
+
+                // It is expected that the Utf8JsonReader won't throw an exception here
+                Assert.True(json.Read());
+                Assert.Equal(JsonTokenType.String, json.TokenType);
+
+                while (json.Read())
+                    ;
+
+                json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state);
+
+                while (json.Read())
+                {
+                    if (json.TokenType == JsonTokenType.String)
+                    {
+                        try
+                        {
+                            byte[] val = json.GetBytesFromBase64();
+                            Assert.True(false, "Expected InvalidOperationException when trying to decode base 64 string for invalid UTF-8 JSON text.");
+                        }
+                        catch (FormatException) { }
+
+                        Assert.False(json.TryGetBytesFromBase64(out byte[] value));
+                        Assert.Null(value);
+                    }
+                }
+            }
+        }
+
         [Theory]
         [MemberData(nameof(GetCommentTestData))]
         public static void TestingGetComment(string jsonData, string expected)
index 2d1e30f..9a046b3 100644 (file)
@@ -1408,6 +1408,50 @@ namespace System.Text.Json.Tests
             Assert.Throws<ArgumentException>(() => jsonUtf8.WriteStartArray(key));
         }
 
+        [ConditionalTheory(nameof(IsX64))]
+        [OuterLoop]
+        [InlineData(true, true)]
+        [InlineData(true, false)]
+        [InlineData(false, true)]
+        [InlineData(false, false)]
+        public void WritingTooLargeBase64Bytes(bool formatted, bool skipValidation)
+        {
+            byte[] value;
+
+            try
+            {
+                value = new byte[200_000_000];
+            }
+            catch (OutOfMemoryException)
+            {
+                return;
+            }
+
+            value.AsSpan().Fill(255);
+
+            var options = new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation };
+            var output = new ArrayBufferWriter<byte>(1024);
+
+            var jsonUtf8 = new Utf8JsonWriter(output, options);
+            Assert.Throws<ArgumentException>(() => jsonUtf8.WriteBase64StringValue(value));
+
+            jsonUtf8 = new Utf8JsonWriter(output, options);
+            jsonUtf8.WriteStartObject();
+            Assert.Throws<ArgumentException>(() => jsonUtf8.WriteBase64String("foo", value));
+
+            jsonUtf8 = new Utf8JsonWriter(output, options);
+            jsonUtf8.WriteStartObject();
+            Assert.Throws<ArgumentException>(() => jsonUtf8.WriteBase64String(Encoding.UTF8.GetBytes("foo"), value));
+
+            jsonUtf8 = new Utf8JsonWriter(output, options);
+            jsonUtf8.WriteStartObject();
+            Assert.Throws<ArgumentException>(() => jsonUtf8.WriteBase64String("foo".AsSpan(), value));
+
+            jsonUtf8 = new Utf8JsonWriter(output, options);
+            jsonUtf8.WriteStartObject();
+            Assert.Throws<ArgumentException>(() => jsonUtf8.WriteBase64String(JsonEncodedText.Encode("foo"), value));
+        }
+
         [Theory]
         [InlineData(true, true)]
         [InlineData(true, false)]
@@ -1703,6 +1747,171 @@ namespace System.Text.Json.Tests
         [InlineData(true, false)]
         [InlineData(false, true)]
         [InlineData(false, false)]
+        public void WriteBase64String(bool formatted, bool skipValidation)
+        {
+            string propertyName = "message";
+            byte[] value = { 1, 2, 3, 4, 5 };
+            string expectedStr = GetBase64ExpectedString(prettyPrint: formatted, propertyName, value);
+
+            JsonEncodedText encodedPropertyName = JsonEncodedText.Encode(propertyName);
+
+            byte[] utf8PropertyName = Encoding.UTF8.GetBytes("message");
+
+            var options = new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation };
+
+            for (int i = 0; i < 4; i++)
+            {
+                var output = new ArrayBufferWriter<byte>(32);
+                var jsonUtf8 = new Utf8JsonWriter(output, options);
+
+                jsonUtf8.WriteStartObject();
+
+                switch (i)
+                {
+                    case 0:
+                        jsonUtf8.WriteBase64String(propertyName, value);
+                        jsonUtf8.WriteBase64String(propertyName, value);
+                        break;
+                    case 1:
+                        jsonUtf8.WriteBase64String(propertyName.AsSpan(), value);
+                        jsonUtf8.WriteBase64String(propertyName.AsSpan(), value);
+                        break;
+                    case 2:
+                        jsonUtf8.WriteBase64String(utf8PropertyName, value);
+                        jsonUtf8.WriteBase64String(utf8PropertyName, value);
+                        break;
+                    case 3:
+                        jsonUtf8.WriteBase64String(encodedPropertyName, value);
+                        jsonUtf8.WriteBase64String(encodedPropertyName, value);
+                        break;
+                }
+
+                jsonUtf8.WriteEndObject();
+                jsonUtf8.Flush();
+
+                AssertContents(expectedStr, output);
+            }
+        }
+
+        [Theory]
+        [InlineData(true, true)]
+        [InlineData(true, false)]
+        [InlineData(false, true)]
+        [InlineData(false, false)]
+        public void WriteBase64StringEscaped(bool formatted, bool skipValidation)
+        {
+            string propertyName = "mess><age";
+            byte[] value = { 1, 2, 3, 4, 5, 6 };
+            string expectedStr = GetBase64ExpectedString(prettyPrint: formatted, propertyName, value);
+
+            var options = new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation };
+
+            ReadOnlySpan<char> propertyNameSpan = propertyName.AsSpan();
+            ReadOnlySpan<byte> propertyNameSpanUtf8 = Encoding.UTF8.GetBytes(propertyName);
+            JsonEncodedText encodedPropertyName = JsonEncodedText.Encode(propertyName);
+
+            for (int i = 0; i < 4; i++)
+            {
+                var output = new ArrayBufferWriter<byte>(32);
+                var jsonUtf8 = new Utf8JsonWriter(output, options);
+
+                jsonUtf8.WriteStartObject();
+
+                switch (i)
+                {
+                    case 0:
+                        jsonUtf8.WriteBase64String(propertyName, value);
+                        jsonUtf8.WriteBase64String(propertyName, value);
+                        break;
+                    case 1:
+                        jsonUtf8.WriteBase64String(propertyNameSpan, value);
+                        jsonUtf8.WriteBase64String(propertyNameSpan, value);
+                        break;
+                    case 2:
+                        jsonUtf8.WriteBase64String(propertyNameSpanUtf8, value);
+                        jsonUtf8.WriteBase64String(propertyNameSpanUtf8, value);
+                        break;
+                    case 3:
+                        jsonUtf8.WriteBase64String(encodedPropertyName, value);
+                        jsonUtf8.WriteBase64String(encodedPropertyName, value);
+                        break;
+                }
+
+                jsonUtf8.WriteEndObject();
+                jsonUtf8.Flush();
+
+                AssertContents(expectedStr, output);
+            }
+
+            // Verify that escaping does not change the input strings/spans.
+            Assert.Equal("mess><age", propertyName);
+            Assert.Equal(new byte[] { 1, 2, 3, 4, 5, 6 }, value);
+            Assert.True(propertyName.AsSpan().SequenceEqual(propertyNameSpan));
+            Assert.True(Encoding.UTF8.GetBytes(propertyName).AsSpan().SequenceEqual(propertyNameSpanUtf8));
+        }
+
+        [Theory]
+        [InlineData(true, true)]
+        [InlineData(true, false)]
+        [InlineData(false, true)]
+        [InlineData(false, false)]
+        public void WritePartialBase64String(bool formatted, bool skipValidation)
+        {
+            var options = new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation };
+
+            var output = new ArrayBufferWriter<byte>(10);
+            var jsonUtf8 = new Utf8JsonWriter(output, options);
+
+            jsonUtf8.WriteStartObject();
+
+            Assert.Equal(0, jsonUtf8.BytesCommitted);
+            Assert.Equal(1, jsonUtf8.BytesPending);
+
+            jsonUtf8.WriteBase64String("message", new byte[] { 201, 153, 199 });
+
+            Assert.Equal(0, jsonUtf8.BytesCommitted);
+            if (formatted)
+                Assert.Equal(17 + 2 + Environment.NewLine.Length + 1, jsonUtf8.BytesPending); // new lines, indentation, white space
+            else
+                Assert.Equal(17, jsonUtf8.BytesPending);
+
+            jsonUtf8.Flush();
+
+            if (formatted)
+                Assert.Equal(17 + 2 + Environment.NewLine.Length + 1, jsonUtf8.BytesCommitted); // new lines, indentation, white space
+            else
+                Assert.Equal(17, jsonUtf8.BytesCommitted);
+
+            Assert.Equal(0, jsonUtf8.BytesPending);
+
+            jsonUtf8.WriteBase64String("message", new byte[] { 201, 153, 199 });
+            jsonUtf8.WriteEndObject();
+
+            if (formatted)
+                Assert.Equal(17 + 2 + Environment.NewLine.Length + 1, jsonUtf8.BytesCommitted);
+            else
+                Assert.Equal(17, jsonUtf8.BytesCommitted);
+
+            if (formatted)
+                Assert.Equal(18 + 2 + (2 * Environment.NewLine.Length) + 1, jsonUtf8.BytesPending); // new lines, indentation, white space
+            else
+                Assert.Equal(18, jsonUtf8.BytesPending);
+
+            jsonUtf8.Flush();
+
+            if (formatted)
+                Assert.Equal(35 + (2 * 2) + (3 * Environment.NewLine.Length) + (1 * 2), jsonUtf8.BytesCommitted); // new lines, indentation, white space
+            else
+                Assert.Equal(35, jsonUtf8.BytesCommitted);
+
+            Assert.Equal(0, jsonUtf8.BytesPending);
+        }
+
+        [Theory]
+        [InlineData(true, true)]
+        [InlineData(true, false)]
+        [InlineData(false, true)]
+        [InlineData(false, false)]
         public void WriteInvalidPartialJson(bool formatted, bool skipValidation)
         {
             var options = new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation };
@@ -1736,6 +1945,111 @@ namespace System.Text.Json.Tests
         [InlineData(true, false)]
         [InlineData(false, true)]
         [InlineData(false, false)]
+        public void WriteInvalidBase64(bool formatted, bool skipValidation)
+        {
+            {
+                var options = new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation };
+                var output = new ArrayBufferWriter<byte>(10);
+                using var jsonUtf8 = new Utf8JsonWriter(output, options);
+
+                jsonUtf8.WriteStartObject();
+
+                Assert.Equal(0, jsonUtf8.BytesCommitted);
+                Assert.Equal(1, jsonUtf8.BytesPending);
+
+                jsonUtf8.Flush();
+
+                Assert.Equal(1, jsonUtf8.BytesCommitted);
+                Assert.Equal(0, jsonUtf8.BytesPending);
+
+                if (skipValidation)
+                {
+                    jsonUtf8.WriteBase64StringValue(new byte[] { 1, 2 });
+                    jsonUtf8.WriteEndArray();
+                }
+                else
+                {
+                    Assert.Throws<InvalidOperationException>(() => jsonUtf8.WriteBase64StringValue(new byte[] { 1, 2 }));
+                    Assert.Throws<InvalidOperationException>(() => jsonUtf8.WriteEndArray());
+                }
+            }
+            {
+                var options = new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation };
+                var output = new ArrayBufferWriter<byte>(10);
+                using var jsonUtf8 = new Utf8JsonWriter(output, options);
+
+                jsonUtf8.WriteStartArray();
+
+                Assert.Equal(0, jsonUtf8.BytesCommitted);
+                Assert.Equal(1, jsonUtf8.BytesPending);
+
+                jsonUtf8.Flush();
+
+                Assert.Equal(1, jsonUtf8.BytesCommitted);
+                Assert.Equal(0, jsonUtf8.BytesPending);
+
+                if (skipValidation)
+                {
+                    jsonUtf8.WriteBase64String("foo", new byte[] { 1, 2 });
+                    jsonUtf8.WriteEndObject();
+                }
+                else
+                {
+                    Assert.Throws<InvalidOperationException>(() => jsonUtf8.WriteBase64String("foo", new byte[] { 1, 2 }));
+                    Assert.Throws<InvalidOperationException>(() => jsonUtf8.WriteEndObject());
+                }
+            }
+        }
+
+        [Fact]
+        public void WriteBase64Escapes()
+        {
+            var output = new ArrayBufferWriter<byte>(10);
+            using var jsonUtf8 = new Utf8JsonWriter(output);
+
+            var bytes = new byte[3] { 0xFB, 0xEF, 0xBE };
+            jsonUtf8.WriteBase64StringValue(bytes);
+
+            jsonUtf8.Flush();
+
+            AssertContents("\"\\u002b\\u002b\\u002b\\u002b\"", output);
+        }
+
+        [Fact]
+        public void WriteBase64EscapesLarge()
+        {
+            var output = new ArrayBufferWriter<byte>(10);
+            using var jsonUtf8 = new Utf8JsonWriter(output);
+
+            var bytes = new byte[200];
+
+            bytes.AsSpan().Fill(100);
+            bytes[4] = 0xFB;
+            bytes[5] = 0xEF;
+            bytes[6] = 0xBE;
+            bytes[15] = 0;
+            bytes[16] = 0x10;
+            bytes[17] = 0xBF;
+
+            jsonUtf8.WriteBase64StringValue(bytes);
+
+            jsonUtf8.Flush();
+
+            var builder = new StringBuilder();
+            builder.Append("\"ZGRkZPvvvmRkZGRkZGRkABC\\u002f");
+            for (int i = 0; i < 60; i++)
+            {
+                builder.Append("ZGRk");
+            }
+            builder.Append("ZGQ=\"");
+            AssertContents(builder.ToString(), output);
+        }
+
+        [Theory]
+        [InlineData(true, true)]
+        [InlineData(true, false)]
+        [InlineData(false, true)]
+        [InlineData(false, false)]
         public void WriteInvalidDepthPartial(bool formatted, bool skipValidation)
         {
             {
@@ -2083,7 +2397,7 @@ namespace System.Text.Json.Tests
         {
             var propertyArray = new char[128];
 
-            char[] specialCases = { '+', '`', (char)0x7F };
+            char[] specialCases = { '+', '`', (char)0x7F, '/' };
             for (int i = 0; i < propertyArray.Length; i++)
             {
                 if (Array.IndexOf(specialCases, (char)i) != -1)
@@ -3939,6 +4253,29 @@ namespace System.Text.Json.Tests
             return Encoding.UTF8.GetString(ms.ToArray());
         }
 
+        private static string GetBase64ExpectedString(bool prettyPrint, string propertyName, byte[] value)
+        {
+            var ms = new MemoryStream();
+            TextWriter streamWriter = new StreamWriter(ms, new UTF8Encoding(false), 1024, true);
+
+            var json = new JsonTextWriter(streamWriter)
+            {
+                Formatting = prettyPrint ? Formatting.Indented : Formatting.None,
+                StringEscapeHandling = StringEscapeHandling.EscapeHtml
+            };
+
+            json.WriteStartObject();
+            json.WritePropertyName(propertyName);
+            json.WriteValue(value);
+            json.WritePropertyName(propertyName);
+            json.WriteValue(value);
+            json.WriteEnd();
+
+            json.Flush();
+
+            return Encoding.UTF8.GetString(ms.ToArray());
+        }
+
         private static string GetCommentExpectedString(bool prettyPrint, string comment)
         {
             var ms = new MemoryStream();