From 6638210a81253697393ddc49711624367c7643a5 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Fri, 16 Jan 2026 14:09:20 +0100 Subject: [PATCH 1/3] Add more Base64 tests --- .../System/Convert.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/Convert.cs b/src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/Convert.cs index c3ba20a5d76d22..6281c42a8ec8cd 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/Convert.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/Convert.cs @@ -8,6 +8,8 @@ using System.Collections.Generic; using Test.Cryptography; +using System.Buffers.Text; +using System.Buffers; namespace System.Tests { @@ -297,6 +299,8 @@ public static void TryFromBase64String(string encoded, byte[] expected) bool success = Convert.TryFromBase64String(encoded, actual, out int bytesWritten); Assert.False(success); Assert.Equal(0, bytesWritten); + + Assert.Equal(OperationStatus.InvalidData, Base64.DecodeFromUtf8(Encoding.UTF8.GetBytes(encoded), actual, out _, out _)); } else { @@ -307,6 +311,10 @@ public static void TryFromBase64String(string encoded, byte[] expected) Assert.True(success); Assert.Equal(expected, actual); Assert.Equal(expected.Length, bytesWritten); + + Assert.Equal(OperationStatus.Done, Base64.DecodeFromUtf8(Encoding.UTF8.GetBytes(encoded), actual, out int bytesConsumed, out bytesWritten)); + Assert.Equal(encoded.Length, bytesConsumed); + Assert.Equal(expected.Length, bytesWritten); } // Buffer too short @@ -316,6 +324,9 @@ public static void TryFromBase64String(string encoded, byte[] expected) bool success = Convert.TryFromBase64String(encoded, actual, out int bytesWritten); Assert.False(success); Assert.Equal(0, bytesWritten); + + Assert.Equal(OperationStatus.DestinationTooSmall, Base64.DecodeFromUtf8(Encoding.UTF8.GetBytes(encoded), actual, out _, out bytesWritten)); + Assert.Equal(actual.Length, bytesWritten); } // Buffer larger than needed @@ -327,6 +338,10 @@ public static void TryFromBase64String(string encoded, byte[] expected) Assert.Equal(99, actual[expected.Length]); Assert.Equal(expected, actual.Take(expected.Length)); Assert.Equal(expected.Length, bytesWritten); + + Assert.Equal(OperationStatus.Done, Base64.DecodeFromUtf8(Encoding.UTF8.GetBytes(encoded), actual, out int bytesConsumed, out bytesWritten)); + Assert.Equal(encoded.Length, bytesConsumed); + Assert.Equal(expected.Length, bytesWritten); } } } From 4b9019d4a51f01760d1eccff5097a3f481448a6a Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Fri, 16 Jan 2026 15:23:13 +0100 Subject: [PATCH 2/3] Fix the edge case + more tests --- .../tests/Base64/Base64DecoderUnitTests.cs | 17 +++++++++++++++++ .../Text/Base64Helper/Base64DecoderHelper.cs | 13 ++++++++++++- .../System/Convert.cs | 6 ++++-- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs index 81f5be0688a6b2..4019db358e0061 100644 --- a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs @@ -875,5 +875,22 @@ public void DecodingWithEmbeddedWhiteSpaceIntoSmallDestination_ActualDestination Assert.Equal(4, written4); Assert.Equal(new byte[] { 1, 2, 3, 4 }, destination4); } + + [Fact] + public void DecodingWithEmbeddedWhiteSpaceIntoSmallDestination_TrailingWhiteSpacesAreConsumed() + { + byte[] input = " 8J+N i f C f jYk="u8.ToArray(); + + // The actual decoded data is 8 bytes long. + // If we provide a destination buffer with 6 bytes, we can decode two blocks (6 bytes) and leave 2 bytes undecoded. + // But even though there are 2 bytes left undecoded, we should still consume as much input as possible, + // such that all trailing whitespace are also consumed. + + byte[] destination = new byte[6]; + Assert.Equal(OperationStatus.DestinationTooSmall, Base64.DecodeFromUtf8(input, destination, out int consumed, out int written)); + Assert.Equal((byte)'j', input[consumed]); // byte right after the spaces + Assert.Equal(destination.Length, written); + Assert.Equal(new byte[] { 240, 159, 141, 137, 240, 159 }, destination); + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Helper/Base64DecoderHelper.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Helper/Base64DecoderHelper.cs index d63377faf110ec..b45d6fffff8ae0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Helper/Base64DecoderHelper.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Helper/Base64DecoderHelper.cs @@ -304,11 +304,22 @@ static OperationStatus InvalidDataFallback(TBase64Decoder decoder, ReadOnlySpan< status = DecodeFrom(decoder, source, bytes, out localConsumed, out int localWritten, isFinalBlock, ignoreWhiteSpace: false); bytesConsumed += localConsumed; bytesWritten += localWritten; - if (status is not OperationStatus.InvalidData) + + if (status is OperationStatus.Done or OperationStatus.NeedMoreData) { break; } + // The DecodeFrom helper will return DestinationTooSmall if the destination is too small, + // regardless of whether it's actually too small once you skip whitespace characters. + // As long as we're making progress, try to trim whitespace characters again and continue. + if (status == OperationStatus.DestinationTooSmall && localConsumed == 0) + { + // If the input contains whitespace in the middle of the next Base64 block AND the destination is small, + // the DecodeFrom helper may be unable to make forward progress. Fall back to block-wise decoding. + return decoder.DecodeWithWhiteSpaceBlockwiseWrapper(decoder, source, bytes, ref bytesConsumed, ref bytesWritten, isFinalBlock); + } + source = source.Slice(localConsumed); bytes = bytes.Slice(localWritten); } diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/Convert.cs b/src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/Convert.cs index 6281c42a8ec8cd..fa785c8d29b5a7 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/Convert.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/Convert.cs @@ -325,8 +325,10 @@ public static void TryFromBase64String(string encoded, byte[] expected) Assert.False(success); Assert.Equal(0, bytesWritten); - Assert.Equal(OperationStatus.DestinationTooSmall, Base64.DecodeFromUtf8(Encoding.UTF8.GetBytes(encoded), actual, out _, out bytesWritten)); - Assert.Equal(actual.Length, bytesWritten); + Assert.Equal(OperationStatus.DestinationTooSmall, Base64.DecodeFromUtf8(Encoding.UTF8.GetBytes(encoded), actual, out int bytesConsumed, out bytesWritten)); + Assert.Equal(actual.Length / 3 * 3, bytesWritten); + Assert.InRange(bytesConsumed, Base64.GetMaxEncodedToUtf8Length(bytesWritten), encoded.Length - 1); + Assert.NotEqual(' ', encoded[bytesConsumed]); } // Buffer larger than needed From 11f2bc63f9e386b4c29fc56fbd6d1b7ae3b8f945 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Tue, 20 Jan 2026 16:49:32 +0100 Subject: [PATCH 3/3] Simplify the change --- .../Buffers/Text/Base64Helper/Base64DecoderHelper.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Helper/Base64DecoderHelper.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Helper/Base64DecoderHelper.cs index b45d6fffff8ae0..9541f13860809c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Helper/Base64DecoderHelper.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Helper/Base64DecoderHelper.cs @@ -312,13 +312,7 @@ static OperationStatus InvalidDataFallback(TBase64Decoder decoder, ReadOnlySpan< // The DecodeFrom helper will return DestinationTooSmall if the destination is too small, // regardless of whether it's actually too small once you skip whitespace characters. - // As long as we're making progress, try to trim whitespace characters again and continue. - if (status == OperationStatus.DestinationTooSmall && localConsumed == 0) - { - // If the input contains whitespace in the middle of the next Base64 block AND the destination is small, - // the DecodeFrom helper may be unable to make forward progress. Fall back to block-wise decoding. - return decoder.DecodeWithWhiteSpaceBlockwiseWrapper(decoder, source, bytes, ref bytesConsumed, ref bytesWritten, isFinalBlock); - } + // In that case we loop again and fall back to block-wise decoding if we can't make progress. source = source.Slice(localConsumed); bytes = bytes.Slice(localWritten);