Spanify Cryptography.Base64Transform
authorGünther Foidl <gue@korporal.at>
Wed, 17 Jul 2019 18:06:17 +0000 (20:06 +0200)
committerJeremy Barton <jbarton@microsoft.com>
Wed, 17 Jul 2019 18:06:17 +0000 (11:06 -0700)
So it can be non-allocating byte[] -> byte[], instead of allocating (temp. buffer) and copying with byte[] -> char[] -> byte[].

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

src/libraries/System.Security.Cryptography.Encoding/src/System.Security.Cryptography.Encoding.csproj
src/libraries/System.Security.Cryptography.Encoding/src/System/Security/Cryptography/Base64Transforms.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Base64TransformsTests.cs

index 8b6ffd9..922ad06 100644 (file)
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <ProjectGuid>{AA81E343-5E54-40B0-9381-C459419BE780}</ProjectGuid>
     <AssemblyName>System.Security.Cryptography.Encoding</AssemblyName>
@@ -23,6 +23,9 @@
     <Compile Include="$(CommonPath)\Internal\Cryptography\Helpers.cs">
       <Link>Internal\Cryptography\Helpers.cs</Link>
     </Compile>
+    <Compile Include="$(CommonPath)\System\Security\Cryptography\CryptoPool.cs">
+      <Link>System\Security\Cryptography\CryptoPool.cs</Link>
+    </Compile>
   </ItemGroup>
   <ItemGroup Condition=" '$(TargetsWindows)' == 'true'">
     <Compile Include="Internal\Cryptography\AsnFormatter.Windows.cs" />
     <Compile Include="$(CommonPath)\System\Memory\PointerMemoryManager.cs">
       <Link>Common\System\Memory\PointerMemoryManager.cs</Link>
     </Compile>
-    <Compile Include="$(CommonPath)\System\Security\Cryptography\CryptoPool.cs">
-      <Link>Common\System\Security\Cryptography\CryptoPool.cs</Link>
-    </Compile>
     <AsnXml Include="$(CommonPath)\System\Security\Cryptography\Asn1\DirectoryStringAsn.xml">
       <Link>Common\System\Security\Cryptography\Asn1\DirectoryStringAsn.xml</Link>
     </AsnXml>
index 4b04bfe..5712a4e 100644 (file)
@@ -5,7 +5,10 @@
 // This file contains two ICryptoTransforms: ToBase64Transform and FromBase64Transform
 // they may be attached to a CryptoStream in either read or write mode
 
-using System.Text;
+using System.Buffers;
+using System.Buffers.Text;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
 
 namespace System.Security.Cryptography
 {
@@ -25,18 +28,33 @@ namespace System.Security.Cryptography
 
         public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
         {
-            ValidateTransformBlock(inputBuffer, inputOffset, inputCount);
+            // inputCount < InputBlockSize is not allowed
+            ThrowHelper.ValidateTransformBlock(inputBuffer, inputOffset, inputCount, InputBlockSize);
+
+            if (outputBuffer == null)
+                ThrowHelper.ThrowArgumentNull(ThrowHelper.ExceptionArgument.outputBuffer);
 
             // For now, only convert 3 bytes to 4
-            byte[] tempBytes = ConvertToBase64(inputBuffer, inputOffset, 3);
+            Span<byte> input = inputBuffer.AsSpan(inputOffset, InputBlockSize);
+            Span<byte> output = outputBuffer.AsSpan(outputOffset, OutputBlockSize);
+
+            OperationStatus status = Base64.EncodeToUtf8(input, output, out int consumed, out int written, isFinalBlock: false);
+
+            if (written != OutputBlockSize)
+            {
+                ThrowHelper.ThrowCryptographicException();
+            }
+
+            Debug.Assert(status == OperationStatus.NeedMoreData);
+            Debug.Assert(consumed == InputBlockSize);
 
-            Buffer.BlockCopy(tempBytes, 0, outputBuffer, outputOffset, tempBytes.Length);
-            return tempBytes.Length;
+            return written;
         }
 
         public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount)
         {
-            ValidateTransformBlock(inputBuffer, inputOffset, inputCount);
+            // inputCount <= InputBlockSize is allowed
+            ThrowHelper.ValidateTransformBlock(inputBuffer, inputOffset, inputCount);
 
             // Convert.ToBase64CharArray already does padding, so all we have to check is that
             // the inputCount wasn't 0
@@ -44,28 +62,26 @@ namespace System.Security.Cryptography
             {
                 return Array.Empty<byte>();
             }
+            else if (inputCount > InputBlockSize)
+            {
+                ThrowHelper.ThrowArgumentOutOfRange(ThrowHelper.ExceptionArgument.inputCount);
+            }
 
             // Again, for now only a block at a time
-            return ConvertToBase64(inputBuffer, inputOffset, inputCount);
-        }
+            Span<byte> input = inputBuffer.AsSpan(inputOffset, inputCount);
+            byte[] output = new byte[OutputBlockSize];
 
-        private byte[] ConvertToBase64(byte[] inputBuffer, int inputOffset, int inputCount)
-        {
-            char[] temp = new char[4];
-            Convert.ToBase64CharArray(inputBuffer, inputOffset, inputCount, temp, 0);
-            byte[] tempBytes = Encoding.ASCII.GetBytes(temp);
-            if (tempBytes.Length != 4)
-                throw new CryptographicException(SR.Cryptography_SSE_InvalidDataSize);
+            OperationStatus status = Base64.EncodeToUtf8(input, output, out int consumed, out int written, isFinalBlock: true);
 
-            return tempBytes;
-        }
+            if (written != OutputBlockSize)
+            {
+                ThrowHelper.ThrowCryptographicException();
+            }
 
-        private static void ValidateTransformBlock(byte[] inputBuffer, int inputOffset, int inputCount)
-        {
-            if (inputBuffer == null) throw new ArgumentNullException(nameof(inputBuffer));
-            if (inputOffset < 0) throw new ArgumentOutOfRangeException(nameof(inputOffset), SR.ArgumentOutOfRange_NeedNonNegNum);
-            if (inputCount < 0 || (inputCount > inputBuffer.Length)) throw new ArgumentException(SR.Argument_InvalidValue);
-            if ((inputBuffer.Length - inputCount) < inputOffset) throw new ArgumentException(SR.Argument_InvalidOffLen);
+            Debug.Assert(status == OperationStatus.Done);
+            Debug.Assert(consumed == inputCount);
+
+            return output;
         }
 
         // Must implement IDisposable, but in this case there's nothing to do.
@@ -96,16 +112,18 @@ namespace System.Security.Cryptography
     {
         private byte[] _inputBuffer = new byte[4];
         private int _inputIndex;
-        private FromBase64TransformMode _whitespaces;
+        private readonly FromBase64TransformMode _whitespaces;
 
         public FromBase64Transform() : this(FromBase64TransformMode.IgnoreWhiteSpaces) { }
         public FromBase64Transform(FromBase64TransformMode whitespaces)
         {
             _whitespaces = whitespaces;
-            _inputIndex = 0;
         }
 
         // Converting from Base64 generates 3 bytes output from each 4 bytes input block
+        private const int Base64InputBlockSize = 4;
+        // A buffer with size 32 is stack allocated, to cover common cases and benefit from JIT's optimizations.
+        private const int StackAllocSize = 32;
         public int InputBlockSize => 1;
         public int OutputBlockSize => 3;
         public bool CanTransformMultipleBlocks => false;
@@ -113,126 +131,210 @@ namespace System.Security.Cryptography
 
         public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
         {
-            ValidateTransformBlock(inputBuffer, inputOffset, inputCount);
-            if (_inputBuffer == null) throw new ObjectDisposedException(null, SR.ObjectDisposed_Generic);
+            // inputCount != InputBlockSize is allowed
+            ThrowHelper.ValidateTransformBlock(inputBuffer, inputOffset, inputCount);
+
+            if (_inputBuffer == null)
+                ThrowHelper.ThrowObjectDisposed();
+
+            if (outputBuffer == null)
+                ThrowHelper.ThrowArgumentNull(ThrowHelper.ExceptionArgument.outputBuffer);
 
-            int effectiveCount;
-            byte[] temp = GetTempBuffer(inputBuffer, inputOffset, inputCount, out effectiveCount);
+            // The common case is inputCount = InputBlockSize
+            byte[] tmpBufferArray = null;
+            Span<byte> tmpBuffer = stackalloc byte[StackAllocSize];
+            if (inputCount > StackAllocSize)
+            {
+                tmpBuffer = tmpBufferArray = CryptoPool.Rent(inputCount);
+            }
 
-            if (effectiveCount + _inputIndex < 4)
+            tmpBuffer = GetTempBuffer(inputBuffer.AsSpan(inputOffset, inputCount), tmpBuffer);
+            int bytesToTransform = _inputIndex + tmpBuffer.Length;
+
+            // Too little data to decode: save data to _inputBuffer, so it can be transformed later
+            if (bytesToTransform < Base64InputBlockSize)
             {
-                Buffer.BlockCopy(temp, 0, _inputBuffer, _inputIndex, effectiveCount);
-                _inputIndex += effectiveCount;
+                tmpBuffer.CopyTo(_inputBuffer.AsSpan(_inputIndex));
+
+                _inputIndex = bytesToTransform;
+
+                ReturnToCryptoPool(tmpBufferArray, tmpBuffer.Length);
+
                 return 0;
             }
 
-            byte[] result = ConvertFromBase64(temp, effectiveCount);
+            ConvertFromBase64(tmpBuffer, outputBuffer.AsSpan(outputOffset), out _, out int written);
 
-            Buffer.BlockCopy(result, 0, outputBuffer, outputOffset, result.Length);
+            ReturnToCryptoPool(tmpBufferArray, tmpBuffer.Length);
 
-            return result.Length;
+            return written;
         }
 
         public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount)
         {
-            ValidateTransformBlock(inputBuffer, inputOffset, inputCount);
-            if (_inputBuffer == null) throw new ObjectDisposedException(null, SR.ObjectDisposed_Generic);
+            // inputCount != InputBlockSize is allowed
+            ThrowHelper.ValidateTransformBlock(inputBuffer, inputOffset, inputCount);
 
-            int effectiveCount;
-            byte[] temp = GetTempBuffer(inputBuffer, inputOffset, inputCount, out effectiveCount);
+            if (_inputBuffer == null)
+            {
+                ThrowHelper.ThrowObjectDisposed();
+            }
+
+            if (inputCount == 0)
+            {
+                return Array.Empty<byte>();
+            }
 
-            if (effectiveCount + _inputIndex < 4)
+            // The common case is inputCount <= Base64InputBlockSize
+            byte[] tmpBufferArray = null;
+            Span<byte> tmpBuffer = stackalloc byte[StackAllocSize];
+            if (inputCount > StackAllocSize)
             {
+                tmpBuffer = tmpBufferArray = CryptoPool.Rent(inputCount);
+            }
+
+            tmpBuffer = GetTempBuffer(inputBuffer.AsSpan(inputOffset, inputCount), tmpBuffer);
+            int bytesToTransform = _inputIndex + tmpBuffer.Length;
+
+            // Too little data to decode
+            if (bytesToTransform < Base64InputBlockSize)
+            {
+                // reinitialize the transform
                 Reset();
+
+                ReturnToCryptoPool(tmpBufferArray, tmpBuffer.Length);
+
                 return Array.Empty<byte>();
             }
 
-            byte[] result = ConvertFromBase64(temp, effectiveCount);
+            int outputSize = GetOutputSize(bytesToTransform, tmpBuffer);
+            byte[] output = new byte[outputSize];
+
+            ConvertFromBase64(tmpBuffer, output, out int consumed, out int written);
+            Debug.Assert(written == outputSize);
+
+            ReturnToCryptoPool(tmpBufferArray, tmpBuffer.Length);
 
             // reinitialize the transform
             Reset();
 
-            return result;
+            return output;
         }
 
-        private byte[] GetTempBuffer(byte[] inputBuffer, int inputOffset, int inputCount, out int effectiveCount)
+        private Span<byte> GetTempBuffer(Span<byte> inputBuffer, Span<byte> tmpBuffer)
         {
-            byte[] temp;
-
-            if (_whitespaces == FromBase64TransformMode.IgnoreWhiteSpaces)
+            if (_whitespaces == FromBase64TransformMode.DoNotIgnoreWhiteSpaces)
             {
-                temp = DiscardWhiteSpaces(inputBuffer, inputOffset, inputCount);
-                effectiveCount = temp.Length;
+                return inputBuffer;
             }
-            else
+
+            return DiscardWhiteSpaces(inputBuffer, tmpBuffer);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static Span<byte> DiscardWhiteSpaces(Span<byte> inputBuffer, Span<byte> tmpBuffer)
+        {
+            int count = 0;
+
+            for (int i = 0; i < inputBuffer.Length; i++)
             {
-                temp = new byte[inputCount];
-                Buffer.BlockCopy(inputBuffer, inputOffset, temp, 0, inputCount);
-                effectiveCount = inputCount;
+                if (!IsWhitespace(inputBuffer[i]))
+                {
+                    tmpBuffer[count++] = inputBuffer[i];
+                }
             }
 
-            return temp;
+            return tmpBuffer.Slice(0, count);
         }
 
-        private byte[] ConvertFromBase64(byte[] temp, int effectiveCount)
+        private static bool IsWhitespace(byte value)
         {
-            // Get the number of 4 bytes blocks to transform
-            int numBlocks = (effectiveCount + _inputIndex) / 4;
-
-            byte[] transformBuffer = new byte[_inputIndex + effectiveCount];
-            Buffer.BlockCopy(_inputBuffer, 0, transformBuffer, 0, _inputIndex);
-            Buffer.BlockCopy(temp, 0, transformBuffer, _inputIndex, effectiveCount);
+            // We assume ASCII encoded data. If there is any non-ASCII char, it is invalid
+            // Base64 and will be caught during decoding.
 
-            _inputIndex = (effectiveCount + _inputIndex) % 4;
-            Buffer.BlockCopy(temp, effectiveCount - _inputIndex, _inputBuffer, 0, _inputIndex);
+            // SPACE        32
+            // TAB           9
+            // LF           10
+            // VTAB         11
+            // FORM FEED    12
+            // CR           13
 
-            char[] tempChar = Encoding.ASCII.GetChars(transformBuffer, 0, 4 * numBlocks);
-            byte[] tempBytes = Convert.FromBase64CharArray(tempChar, 0, 4 * numBlocks);
-            return tempBytes;
+            return value == 32 || ((uint)value - 9 <= (13 - 9));
         }
 
-        private byte[] DiscardWhiteSpaces(byte[] inputBuffer, int inputOffset, int inputCount)
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private int GetOutputSize(int bytesToTransform, Span<byte> tmpBuffer)
         {
-            int i, iCount = 0;
-            for (i = 0; i < inputCount; i++)
+            int outputSize = Base64.GetMaxDecodedFromUtf8Length(bytesToTransform);
+
+            const byte padding = (byte)'=';
+            int len = tmpBuffer.Length;
+
+            // In Base64 there are maximum 2 padding chars
+
+            if (tmpBuffer[len - 2] == padding)
             {
-                if (char.IsWhiteSpace((char)inputBuffer[inputOffset + i])) iCount++;
+                outputSize--;
             }
 
-            // If there's nothing to do, leave early
-            if (iCount == 0 && inputOffset == 0 &&
-                inputCount == inputBuffer.Length)
+            if (tmpBuffer[len - 1] == padding)
             {
-                return inputBuffer;
+                outputSize--;
             }
 
-            byte[] rgbOut = new byte[inputCount - iCount];
-            iCount = 0;
-            for (i = 0; i < inputCount; i++)
+            return outputSize;
+        }
+
+        private void ConvertFromBase64(Span<byte> tmpBuffer, Span<byte> outputBuffer, out int consumed, out int written)
+        {
+            int bytesToTransform = _inputIndex + tmpBuffer.Length;
+            Debug.Assert(bytesToTransform >= 4);
+
+            byte[] transformBufferArray = null;
+            Span<byte> transformBuffer = stackalloc byte[StackAllocSize];
+            if (bytesToTransform > StackAllocSize)
             {
-                if (!char.IsWhiteSpace((char)inputBuffer[inputOffset + i]))
-                {
-                    rgbOut[iCount++] = inputBuffer[inputOffset + i];
-                }
+                transformBuffer = transformBufferArray = CryptoPool.Rent(bytesToTransform);
+            }
+
+            // Copy _inputBuffer to transformBuffer and append tmpBuffer
+            Debug.Assert(_inputIndex < _inputBuffer.Length);
+            _inputBuffer.AsSpan(0, _inputIndex).CopyTo(transformBuffer);
+            tmpBuffer.CopyTo(transformBuffer.Slice(_inputIndex));
+
+            // Save data that won't be transformed to _inputBuffer, so it can be transformed later
+            _inputIndex = bytesToTransform & 3;     // bit hack for % 4
+            bytesToTransform -= _inputIndex;        // only transform up to the next multiple of 4
+            Debug.Assert(_inputIndex < _inputBuffer.Length);
+            tmpBuffer.Slice(tmpBuffer.Length - _inputIndex).CopyTo(_inputBuffer);
+
+            transformBuffer = transformBuffer.Slice(0, bytesToTransform);
+            OperationStatus status = Base64.DecodeFromUtf8(transformBuffer, outputBuffer, out consumed, out written);
+
+            if (status == OperationStatus.Done)
+            {
+                Debug.Assert(consumed == bytesToTransform);
+            }
+            else
+            {
+                Debug.Assert(status == OperationStatus.InvalidData);
+                ThrowHelper.ThrowBase64FormatException();
             }
 
-            return rgbOut;
+            ReturnToCryptoPool(transformBufferArray, transformBuffer.Length);
         }
 
-        private static void ValidateTransformBlock(byte[] inputBuffer, int inputOffset, int inputCount)
+        private void ReturnToCryptoPool(byte[] array, int clearSize)
         {
-            if (inputBuffer == null) throw new ArgumentNullException(nameof(inputBuffer));
-            if (inputOffset < 0) throw new ArgumentOutOfRangeException(nameof(inputOffset), SR.ArgumentOutOfRange_NeedNonNegNum);
-            if (inputCount < 0 || (inputCount > inputBuffer.Length)) throw new ArgumentException(SR.Argument_InvalidValue);
-            if ((inputBuffer.Length - inputCount) < inputOffset) throw new ArgumentException(SR.Argument_InvalidOffLen);
+            if (array != null)
+            {
+                CryptoPool.Return(array, clearSize);
+            }
         }
 
-        // must implement IDisposable, which in this case means clearing the input buffer
-
-        public void Dispose()
+        public void Clear()
         {
-            Dispose(true);
-            GC.SuppressFinalize(this);
+            Dispose();
         }
 
         // Reset the state of the transform so it can be used again
@@ -241,9 +343,12 @@ namespace System.Security.Cryptography
             _inputIndex = 0;
         }
 
-        public void Clear()
+        // must implement IDisposable, which in this case means clearing the input buffer
+
+        public void Dispose()
         {
-            Dispose();
+            Dispose(true);
+            GC.SuppressFinalize(this);
         }
 
         protected virtual void Dispose(bool disposing)
@@ -252,9 +357,12 @@ namespace System.Security.Cryptography
             if (disposing)
             {
                 if (_inputBuffer != null)
-                    Array.Clear(_inputBuffer, 0, _inputBuffer.Length);
-                _inputBuffer = null;
-                _inputIndex = 0;
+                {
+                    CryptographicOperations.ZeroMemory(_inputBuffer);
+                    _inputBuffer = null;
+                }
+
+                Reset();
             }
         }
 
@@ -263,4 +371,46 @@ namespace System.Security.Cryptography
             Dispose(false);
         }
     }
+
+    internal class ThrowHelper
+    {
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static void ValidateTransformBlock(byte[] inputBuffer, int inputOffset, int inputCount)
+        {
+            if (inputBuffer == null)
+                ThrowArgumentNull(ExceptionArgument.inputBuffer);
+
+            if ((uint)inputCount > inputBuffer.Length)
+                ThrowArgumentOutOfRange(ExceptionArgument.inputCount);
+
+            if (inputOffset < 0)
+                ThrowArgumentOutOfRange(ExceptionArgument.inputOffset);
+
+            if ((inputBuffer.Length - inputCount) < inputOffset)
+                ThrowInvalidOffLen();
+        }
+
+        public static void ValidateTransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, int inputBlockSize)
+        {
+            ValidateTransformBlock(inputBuffer, inputOffset, inputCount);
+
+            if (inputCount < inputBlockSize)
+                ThrowArgumentOutOfRange(ExceptionArgument.inputCount);
+        }
+
+        public static void ThrowArgumentNull(ExceptionArgument argument) => throw new ArgumentNullException(argument.ToString());
+        public static void ThrowArgumentOutOfRange(ExceptionArgument argument) => throw new ArgumentOutOfRangeException(argument.ToString(), SR.ArgumentOutOfRange_NeedNonNegNum);
+        public static void ThrowInvalidOffLen() => throw new ArgumentException(SR.Argument_InvalidOffLen);
+        public static void ThrowObjectDisposed() => throw new ObjectDisposedException(null, SR.ObjectDisposed_Generic);
+        public static void ThrowCryptographicException() => throw new CryptographicException(SR.Cryptography_SSE_InvalidDataSize);
+        public static void ThrowBase64FormatException() => throw new FormatException();
+
+        public enum ExceptionArgument
+        {
+            inputBuffer,
+            outputBuffer,
+            inputOffset,
+            inputCount
+        }
+    }
 }
index 0e8d6d1..d23a9ef 100644 (file)
@@ -4,7 +4,6 @@
 
 using System.Collections.Generic;
 using System.IO;
-using Test.Cryptography;
 using Xunit;
 
 namespace System.Security.Cryptography.Encoding.Tests
@@ -39,7 +38,9 @@ namespace System.Security.Cryptography.Encoding.Tests
 
         public static IEnumerable<object[]> TestData_Ascii_Whitespace()
         {
+            yield return new object[] { "fo", "\tZ\tm8=\r" };
             yield return new object[] { "fo", "\tZ\tm8=\n" };
+            yield return new object[] { "fo", "\tZ\tm8=\r\n" };
             yield return new object[] { "foo", " Z m 9 v" };
         }
 
@@ -49,6 +50,7 @@ namespace System.Security.Cryptography.Encoding.Tests
             yield return new object[] { "Zm9v////", 0, 4, "foo" };
             yield return new object[] { "////Zm9v", 4, 4, "foo" };
             yield return new object[] { "////Zm9v////", 4, 4, "foo" };
+            yield return new object[] { "Zm9vYmFyYm", 0, 10, "foobar" };
         }
 
         [Fact]
@@ -61,7 +63,7 @@ namespace System.Security.Cryptography.Encoding.Tests
                 InvalidInput_Base64Transform(transform);
 
                 // These exceptions only thrown in ToBase
-                AssertExtensions.Throws<ArgumentOutOfRangeException>("offsetOut", () => transform.TransformFinalBlock(data_5bytes, 0, 5));
+                AssertExtensions.Throws<ArgumentOutOfRangeException>("inputCount", () => transform.TransformFinalBlock(data_5bytes, 0, 5));
             }
         }
 
@@ -85,8 +87,8 @@ namespace System.Security.Cryptography.Encoding.Tests
 
             AssertExtensions.Throws<ArgumentNullException>("inputBuffer", () => transform.TransformBlock(null, 0, 0, null, 0));
             AssertExtensions.Throws<ArgumentOutOfRangeException>("inputOffset", () => transform.TransformBlock(Array.Empty<byte>(), -1, 0, null, 0));
-            AssertExtensions.Throws<ArgumentNullException>("dst", () => transform.TransformBlock(data_4bytes, 0, 4, null, 0));
-            AssertExtensions.Throws<ArgumentException>(null, () => transform.TransformBlock(Array.Empty<byte>(), 0, 1, null, 0));
+            AssertExtensions.Throws<ArgumentNullException>("outputBuffer", () => transform.TransformBlock(data_4bytes, 0, 4, null, 0));
+            AssertExtensions.Throws<ArgumentOutOfRangeException>("inputCount", () => transform.TransformBlock(Array.Empty<byte>(), 0, 1, null, 0));
             AssertExtensions.Throws<ArgumentException>(null, () => transform.TransformBlock(Array.Empty<byte>(), 1, 0, null, 0));
 
             AssertExtensions.Throws<ArgumentNullException>("inputBuffer", () => transform.TransformFinalBlock(null, 0, 0));
@@ -147,16 +149,16 @@ namespace System.Security.Cryptography.Encoding.Tests
                 Assert.True(inputBytes.Length > 4);
 
                 // Test passing blocks > 4 characters to TransformFinalBlock (not supported)
-                AssertExtensions.Throws<ArgumentOutOfRangeException>("offsetOut", () => transform.TransformFinalBlock(inputBytes, 0, inputBytes.Length));
+                AssertExtensions.Throws<ArgumentOutOfRangeException>("inputCount", () => transform.TransformFinalBlock(inputBytes, 0, inputBytes.Length));
             }
         }
 
         [Theory, MemberData(nameof(TestData_LongBlock_Ascii))]
-        public static void ValidateFromBase64TransformFinalBlock(string expected, string encoding)
+        public static void ValidateFromBase64TransformFinalBlock(string expected, string encoded)
         {
             using (var transform = new FromBase64Transform())
             {
-                byte[] inputBytes = Text.Encoding.ASCII.GetBytes(encoding);
+                byte[] inputBytes = Text.Encoding.ASCII.GetBytes(encoded);
                 Assert.True(inputBytes.Length > 4);
 
                 // Test passing blocks > 4 characters to TransformFinalBlock (supported)
@@ -166,6 +168,22 @@ namespace System.Security.Cryptography.Encoding.Tests
             }
         }
 
+        [Theory, MemberData(nameof(TestData_LongBlock_Ascii))]
+        public static void ValidateFromBase64TransformBlock(string expected, string encoded)
+        {
+            using (var transform = new FromBase64Transform())
+            {
+                byte[] inputBytes = Text.Encoding.ASCII.GetBytes(encoded);
+                Assert.True(inputBytes.Length > 4);
+
+                byte[] outputBytes = new byte[100];
+                int bytesWritten = transform.TransformBlock(inputBytes, 0, inputBytes.Length, outputBytes, 0);
+                string outputText = Text.Encoding.ASCII.GetString(outputBytes, 0, bytesWritten);
+
+                Assert.Equal(expected, outputText);
+            }
+        }
+
         [Theory, MemberData(nameof(TestData_Ascii_NoPadding))]
         public static void ValidateFromBase64_NoPadding(string data)
         {
@@ -208,7 +226,7 @@ namespace System.Security.Cryptography.Encoding.Tests
             byte[] outputBytes = new byte[100];
 
             // Verify default of FromBase64TransformMode.IgnoreWhiteSpaces
-            using (var base64Transform = new FromBase64Transform()) 
+            using (var base64Transform = new FromBase64Transform())
             using (var ms = new MemoryStream(inputBytes))
             using (var cs = new CryptoStream(ms, base64Transform, CryptoStreamMode.Read))
             {