From 80e6ec5a640a2ea57592a21f85bc9783875d6dd7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 17:49:27 +0000 Subject: [PATCH 01/12] Initial plan From 15310dba7443bd8741b34f7f4a2a6d90b0251c81 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 18:20:28 +0000 Subject: [PATCH 02/12] Fix Base64.DecodeFromUtf8 to not consume whitespace in incomplete final quantum when isFinalBlock=false Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com> --- .../tests/Base64/Base64DecoderUnitTests.cs | 77 +++++++++++++++++++ .../Text/Base64Helper/Base64DecoderHelper.cs | 8 +- 2 files changed, 82 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..09ad0d3b0a63e9 100644 --- a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs @@ -875,5 +875,82 @@ public void DecodingWithEmbeddedWhiteSpaceIntoSmallDestination_ActualDestination Assert.Equal(4, written4); Assert.Equal(new byte[] { 1, 2, 3, 4 }, destination4); } + + [Theory] + [InlineData("AA\r\nA=")] + [InlineData("AA\r\nA=\r\n")] + [InlineData("AA A=")] + [InlineData("AA\tA=")] + public void DecodingWithWhiteSpaceSplitFinalQuantumAndIsFinalBlockFalse(string base64String) + { + // When a final quantum (containing padding) is split by whitespace and isFinalBlock=false, + // the decoder should not consume any bytes, allowing the caller to retry with isFinalBlock=true + ReadOnlySpan base64Data = Encoding.ASCII.GetBytes(base64String); + var output = new byte[10]; + + // First call with isFinalBlock=false should consume 0 bytes + OperationStatus status = Base64.DecodeFromUtf8(base64Data, output, out int bytesConsumed, out int bytesWritten, isFinalBlock: false); + Assert.Equal(0, bytesConsumed); + Assert.Equal(0, bytesWritten); + Assert.Equal(OperationStatus.InvalidData, status); + + // Second call with isFinalBlock=true should succeed + status = Base64.DecodeFromUtf8(base64Data, output, out bytesConsumed, out bytesWritten, isFinalBlock: true); + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(base64Data.Length, bytesConsumed); + Assert.Equal(2, bytesWritten); // "AAA=" decodes to 2 bytes + Assert.Equal(new byte[] { 0, 0 }, output[..2]); + } + + [Fact] + public void DecodingWithWhiteSpaceSplitFinalQuantumStreamingScenario() + { + // Simulate a streaming scenario where we call DecodeFromUtf8 multiple times + ReadOnlySpan base64Data = "AA\r\nA="u8; + var output = new byte[10]; + + // First call with isFinalBlock=false + OperationStatus status = Base64.DecodeFromUtf8(base64Data, output, out int bytesConsumed, out int bytesWritten, isFinalBlock: false); + Assert.Equal(OperationStatus.InvalidData, status); + Assert.Equal(0, bytesConsumed); + Assert.Equal(0, bytesWritten); + + // Slice the buffer based on what was consumed (nothing in this case) + base64Data = base64Data.Slice(bytesConsumed); + var outputSpan = output.AsSpan().Slice(bytesWritten); + + // Second call with isFinalBlock=true should successfully decode + status = Base64.DecodeFromUtf8(base64Data, outputSpan, out bytesConsumed, out bytesWritten, isFinalBlock: true); + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(6, bytesConsumed); // All 6 bytes: "AA\r\nA=" + Assert.Equal(2, bytesWritten); // "AAA=" decodes to 2 bytes + } + + [Theory] + [InlineData("AAAA")] + [InlineData("AAA=")] + public void DecodingWithIsFinalBlockFalseNoWhiteSpace(string base64String) + { + // Verify existing behavior is preserved for cases without whitespace + ReadOnlySpan base64Data = Encoding.ASCII.GetBytes(base64String); + var output = new byte[10]; + + if (base64String == "AAAA") + { + // Complete quantum without padding should be decoded + OperationStatus status = Base64.DecodeFromUtf8(base64Data, output, out int bytesConsumed, out int bytesWritten, isFinalBlock: false); + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(4, bytesConsumed); + Assert.Equal(3, bytesWritten); + } + else if (base64String == "AAA=") + { + // Quantum with padding should not be decoded when isFinalBlock=false + OperationStatus status = Base64.DecodeFromUtf8(base64Data, output, out int bytesConsumed, out int bytesWritten, isFinalBlock: false); + Assert.Equal(OperationStatus.InvalidData, status); + Assert.Equal(0, bytesConsumed); + Assert.Equal(0, bytesWritten); + } + } } } 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..6d9031b6412922 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 @@ -477,10 +477,10 @@ internal static OperationStatus DecodeWithWhiteSpaceBlockwise(TB } source = source.Slice(encodedIdx); - bytesConsumed += skipped; if (bufferIdx == 0) { + bytesConsumed += skipped; continue; } @@ -514,14 +514,16 @@ internal static OperationStatus DecodeWithWhiteSpaceBlockwise(TB } status = DecodeFrom(decoder, buffer.Slice(0, bufferIdx), bytes, out int localConsumed, out int localWritten, localIsFinalBlock, ignoreWhiteSpace: false); - bytesConsumed += localConsumed; - bytesWritten += localWritten; if (status != OperationStatus.Done) { return status; } + bytesConsumed += skipped; + bytesConsumed += localConsumed; + bytesWritten += localWritten; + // The remaining data must all be whitespace in order to be valid. if (!hasAnotherBlock) { From 511e441ea5c16d0b0090aafbd49176c286d865ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 18:22:41 +0000 Subject: [PATCH 03/12] Refactor tests: split combined test into separate methods for clarity Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com> --- .../tests/Base64/Base64DecoderUnitTests.cs | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs index 09ad0d3b0a63e9..887b78a02ff851 100644 --- a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs @@ -926,31 +926,30 @@ public void DecodingWithWhiteSpaceSplitFinalQuantumStreamingScenario() Assert.Equal(2, bytesWritten); // "AAA=" decodes to 2 bytes } - [Theory] - [InlineData("AAAA")] - [InlineData("AAA=")] - public void DecodingWithIsFinalBlockFalseNoWhiteSpace(string base64String) + [Fact] + public void DecodingCompleteQuantumWithIsFinalBlockFalse() { - // Verify existing behavior is preserved for cases without whitespace - ReadOnlySpan base64Data = Encoding.ASCII.GetBytes(base64String); + // Complete quantum without padding should be decoded even when isFinalBlock=false + ReadOnlySpan base64Data = "AAAA"u8; var output = new byte[10]; - if (base64String == "AAAA") - { - // Complete quantum without padding should be decoded - OperationStatus status = Base64.DecodeFromUtf8(base64Data, output, out int bytesConsumed, out int bytesWritten, isFinalBlock: false); - Assert.Equal(OperationStatus.Done, status); - Assert.Equal(4, bytesConsumed); - Assert.Equal(3, bytesWritten); - } - else if (base64String == "AAA=") - { - // Quantum with padding should not be decoded when isFinalBlock=false - OperationStatus status = Base64.DecodeFromUtf8(base64Data, output, out int bytesConsumed, out int bytesWritten, isFinalBlock: false); - Assert.Equal(OperationStatus.InvalidData, status); - Assert.Equal(0, bytesConsumed); - Assert.Equal(0, bytesWritten); - } + OperationStatus status = Base64.DecodeFromUtf8(base64Data, output, out int bytesConsumed, out int bytesWritten, isFinalBlock: false); + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(4, bytesConsumed); + Assert.Equal(3, bytesWritten); + } + + [Fact] + public void DecodingPaddedQuantumWithIsFinalBlockFalse() + { + // Quantum with padding should not be decoded when isFinalBlock=false + ReadOnlySpan base64Data = "AAA="u8; + var output = new byte[10]; + + OperationStatus status = Base64.DecodeFromUtf8(base64Data, output, out int bytesConsumed, out int bytesWritten, isFinalBlock: false); + Assert.Equal(OperationStatus.InvalidData, status); + Assert.Equal(0, bytesConsumed); + Assert.Equal(0, bytesWritten); } } } From 72763e0a0514146d390a1920637c51a4ce87333e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 19:29:50 +0000 Subject: [PATCH 04/12] Add debug assert and address review feedback Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com> --- .../System.Memory/tests/Base64/Base64DecoderUnitTests.cs | 1 + .../src/System/Buffers/Text/Base64Helper/Base64DecoderHelper.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs index 887b78a02ff851..061c9e2505fb5e 100644 --- a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs @@ -951,5 +951,6 @@ public void DecodingPaddedQuantumWithIsFinalBlockFalse() Assert.Equal(0, bytesConsumed); Assert.Equal(0, bytesWritten); } + } } 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 6d9031b6412922..6f1d50512d4705 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 @@ -517,6 +517,7 @@ internal static OperationStatus DecodeWithWhiteSpaceBlockwise(TB if (status != OperationStatus.Done) { + Debug.Assert(localConsumed == 0 && localWritten == 0, "On failure, should not have consumed or written any bytes"); return status; } From 2f3d36888309346f6c287a2c0d352775eea5a9a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 19:48:22 +0000 Subject: [PATCH 05/12] Add test cases with valid data before whitespace-split final quantum Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com> --- .../tests/Base64/Base64DecoderUnitTests.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs index 061c9e2505fb5e..30734ebc0d2bba 100644 --- a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs @@ -952,5 +952,50 @@ public void DecodingPaddedQuantumWithIsFinalBlockFalse() Assert.Equal(0, bytesWritten); } + [Theory] + [InlineData("AAAAAAAA AA\r\nA=")] // Two complete blocks, then whitespace-split final quantum + [InlineData("AAAA AAAA AA\r\nA=")] // Two blocks with space, then whitespace-split final quantum + [InlineData("AAAAAAAA\r\nAAAA AA\r\nA=")] // Multiple blocks with various whitespace patterns + public void DecodingWithValidDataBeforeWhiteSpaceSplitFinalQuantum(string base64String) + { + // When there's valid data before a whitespace-split final quantum and isFinalBlock=false, + // verify the streaming scenario works correctly + ReadOnlySpan base64Data = Encoding.ASCII.GetBytes(base64String); + var output = new byte[100]; // Use larger buffer to avoid #123222 + + // First call with isFinalBlock=false + OperationStatus status = Base64.DecodeFromUtf8(base64Data, output, out int bytesConsumed, out int bytesWritten, isFinalBlock: false); + + // Should have processed some data (either Done with partial decode or InvalidData/NeedMoreData) + Assert.True(status == OperationStatus.Done || status == OperationStatus.InvalidData || status == OperationStatus.NeedMoreData); + + // Verify we can complete decoding by retrying with the FULL input and isFinalBlock=true + Array.Clear(output, 0, output.Length); + status = Base64.DecodeFromUtf8(base64Data, output, out bytesConsumed, out bytesWritten, isFinalBlock: true); + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(base64Data.Length, bytesConsumed); + Assert.True(bytesWritten > 0, "Should have decoded data"); + } + + [Theory] + [InlineData("AAAA AA\r\nA=")] // Just base64 start, then whitespace-split final quantum + [InlineData("AA AA AAAA AA\r\nA=")] // Previous blocks with spaces, then whitespace-split final quantum + public void DecodingWithSpacesBeforeWhiteSpaceSplitFinalQuantum(string base64String) + { + // Test cases where previous blocks already contain spaces + ReadOnlySpan base64Data = Encoding.ASCII.GetBytes(base64String); + var output = new byte[100]; // Use larger buffer to avoid #123222 + + // First call with isFinalBlock=false + OperationStatus status = Base64.DecodeFromUtf8(base64Data, output, out int bytesConsumed, out int bytesWritten, isFinalBlock: false); + + // Verify we can complete decoding by retrying with the FULL input and isFinalBlock=true + Array.Clear(output, 0, output.Length); + status = Base64.DecodeFromUtf8(base64Data, output, out bytesConsumed, out bytesWritten, isFinalBlock: true); + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(base64Data.Length, bytesConsumed); + Assert.True(bytesWritten > 0, "Should have decoded data"); + } + } } From c79741efe3df2e8f55f3eb2895550275a22cdc05 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Sat, 17 Jan 2026 21:00:08 +0100 Subject: [PATCH 06/12] Apply suggestion from @MihaZupan --- .../tests/Base64/Base64DecoderUnitTests.cs | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs index 30734ebc0d2bba..c185289899e9d2 100644 --- a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs @@ -902,30 +902,6 @@ public void DecodingWithWhiteSpaceSplitFinalQuantumAndIsFinalBlockFalse(string b Assert.Equal(new byte[] { 0, 0 }, output[..2]); } - [Fact] - public void DecodingWithWhiteSpaceSplitFinalQuantumStreamingScenario() - { - // Simulate a streaming scenario where we call DecodeFromUtf8 multiple times - ReadOnlySpan base64Data = "AA\r\nA="u8; - var output = new byte[10]; - - // First call with isFinalBlock=false - OperationStatus status = Base64.DecodeFromUtf8(base64Data, output, out int bytesConsumed, out int bytesWritten, isFinalBlock: false); - Assert.Equal(OperationStatus.InvalidData, status); - Assert.Equal(0, bytesConsumed); - Assert.Equal(0, bytesWritten); - - // Slice the buffer based on what was consumed (nothing in this case) - base64Data = base64Data.Slice(bytesConsumed); - var outputSpan = output.AsSpan().Slice(bytesWritten); - - // Second call with isFinalBlock=true should successfully decode - status = Base64.DecodeFromUtf8(base64Data, outputSpan, out bytesConsumed, out bytesWritten, isFinalBlock: true); - Assert.Equal(OperationStatus.Done, status); - Assert.Equal(6, bytesConsumed); // All 6 bytes: "AA\r\nA=" - Assert.Equal(2, bytesWritten); // "AAA=" decodes to 2 bytes - } - [Fact] public void DecodingCompleteQuantumWithIsFinalBlockFalse() { From a467bc1f9ab225bd82e1d49758331ea9f22cdbaa Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Sat, 17 Jan 2026 21:00:16 +0100 Subject: [PATCH 07/12] Apply suggestion from @MihaZupan --- .../tests/Base64/Base64DecoderUnitTests.cs | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs index c185289899e9d2..6d53b669884858 100644 --- a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs @@ -952,26 +952,5 @@ public void DecodingWithValidDataBeforeWhiteSpaceSplitFinalQuantum(string base64 Assert.Equal(base64Data.Length, bytesConsumed); Assert.True(bytesWritten > 0, "Should have decoded data"); } - - [Theory] - [InlineData("AAAA AA\r\nA=")] // Just base64 start, then whitespace-split final quantum - [InlineData("AA AA AAAA AA\r\nA=")] // Previous blocks with spaces, then whitespace-split final quantum - public void DecodingWithSpacesBeforeWhiteSpaceSplitFinalQuantum(string base64String) - { - // Test cases where previous blocks already contain spaces - ReadOnlySpan base64Data = Encoding.ASCII.GetBytes(base64String); - var output = new byte[100]; // Use larger buffer to avoid #123222 - - // First call with isFinalBlock=false - OperationStatus status = Base64.DecodeFromUtf8(base64Data, output, out int bytesConsumed, out int bytesWritten, isFinalBlock: false); - - // Verify we can complete decoding by retrying with the FULL input and isFinalBlock=true - Array.Clear(output, 0, output.Length); - status = Base64.DecodeFromUtf8(base64Data, output, out bytesConsumed, out bytesWritten, isFinalBlock: true); - Assert.Equal(OperationStatus.Done, status); - Assert.Equal(base64Data.Length, bytesConsumed); - Assert.True(bytesWritten > 0, "Should have decoded data"); - } - } } From 3921a39a7789ec40a35a172733cfdb9466342359 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 20:06:14 +0000 Subject: [PATCH 08/12] Address review feedback: use non-zero test data and add specific assertions Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com> --- .../tests/Base64/Base64DecoderUnitTests.cs | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs index 6d53b669884858..9346eabf144dbb 100644 --- a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs @@ -877,10 +877,10 @@ public void DecodingWithEmbeddedWhiteSpaceIntoSmallDestination_ActualDestination } [Theory] - [InlineData("AA\r\nA=")] - [InlineData("AA\r\nA=\r\n")] - [InlineData("AA A=")] - [InlineData("AA\tA=")] + [InlineData("AQ\r\nQ=")] + [InlineData("AQ\r\nQ=\r\n")] + [InlineData("AQ Q=")] + [InlineData("AQ\tQ=")] public void DecodingWithWhiteSpaceSplitFinalQuantumAndIsFinalBlockFalse(string base64String) { // When a final quantum (containing padding) is split by whitespace and isFinalBlock=false, @@ -898,8 +898,8 @@ public void DecodingWithWhiteSpaceSplitFinalQuantumAndIsFinalBlockFalse(string b status = Base64.DecodeFromUtf8(base64Data, output, out bytesConsumed, out bytesWritten, isFinalBlock: true); Assert.Equal(OperationStatus.Done, status); Assert.Equal(base64Data.Length, bytesConsumed); - Assert.Equal(2, bytesWritten); // "AAA=" decodes to 2 bytes - Assert.Equal(new byte[] { 0, 0 }, output[..2]); + Assert.Equal(2, bytesWritten); // "AQQ=" decodes to 2 bytes: {1, 4} + Assert.Equal(new byte[] { 1, 4 }, output[..2]); } [Fact] @@ -929,21 +929,27 @@ public void DecodingPaddedQuantumWithIsFinalBlockFalse() } [Theory] - [InlineData("AAAAAAAA AA\r\nA=")] // Two complete blocks, then whitespace-split final quantum - [InlineData("AAAA AAAA AA\r\nA=")] // Two blocks with space, then whitespace-split final quantum - [InlineData("AAAAAAAA\r\nAAAA AA\r\nA=")] // Multiple blocks with various whitespace patterns - public void DecodingWithValidDataBeforeWhiteSpaceSplitFinalQuantum(string base64String) + [InlineData("AQIDBAUG AQ\r\nQ=", 10, 6, "AQ\r\nQ=")] // Two complete blocks, then whitespace-split final quantum + [InlineData("AQID BAUG AQ\r\nQ=", 11, 6, "AQ\r\nQ=")] // Two blocks with space, then whitespace-split final quantum + [InlineData("AQIDBAUG\r\nAQID AQ\r\nQ=", 17, 9, "AQ\r\nQ=")] // Multiple blocks with various whitespace patterns + public void DecodingWithValidDataBeforeWhiteSpaceSplitFinalQuantum(string base64String, int expectedBytesConsumedFirstCall, int expectedBytesWrittenFirstCall, string expectedRemainingAfterFirstCall) { // When there's valid data before a whitespace-split final quantum and isFinalBlock=false, // verify the streaming scenario works correctly ReadOnlySpan base64Data = Encoding.ASCII.GetBytes(base64String); var output = new byte[100]; // Use larger buffer to avoid #123222 - // First call with isFinalBlock=false + // First call with isFinalBlock=false should decode the valid complete blocks and stop before the incomplete final quantum OperationStatus status = Base64.DecodeFromUtf8(base64Data, output, out int bytesConsumed, out int bytesWritten, isFinalBlock: false); - // Should have processed some data (either Done with partial decode or InvalidData/NeedMoreData) - Assert.True(status == OperationStatus.Done || status == OperationStatus.InvalidData || status == OperationStatus.NeedMoreData); + Assert.Equal(OperationStatus.InvalidData, status); + Assert.Equal(expectedBytesConsumedFirstCall, bytesConsumed); + Assert.Equal(expectedBytesWrittenFirstCall, bytesWritten); + + // Verify that only the final block remains + ReadOnlySpan remaining = base64Data.Slice(bytesConsumed); + string remainingString = Encoding.ASCII.GetString(remaining); + Assert.Equal(expectedRemainingAfterFirstCall, remainingString); // Verify we can complete decoding by retrying with the FULL input and isFinalBlock=true Array.Clear(output, 0, output.Length); From c7a85ef02c70f840abb8b382bd168ff4e558e2ff Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Sat, 17 Jan 2026 21:11:32 +0100 Subject: [PATCH 09/12] Apply suggestion from @MihaZupan --- .../System.Memory/tests/Base64/Base64DecoderUnitTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs index 9346eabf144dbb..ab2c9286227ecc 100644 --- a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs @@ -937,7 +937,7 @@ public void DecodingWithValidDataBeforeWhiteSpaceSplitFinalQuantum(string base64 // When there's valid data before a whitespace-split final quantum and isFinalBlock=false, // verify the streaming scenario works correctly ReadOnlySpan base64Data = Encoding.ASCII.GetBytes(base64String); - var output = new byte[100]; // Use larger buffer to avoid #123222 + var output = new byte[100]; // First call with isFinalBlock=false should decode the valid complete blocks and stop before the incomplete final quantum OperationStatus status = Base64.DecodeFromUtf8(base64Data, output, out int bytesConsumed, out int bytesWritten, isFinalBlock: false); From 72b87b876434d058752afbbd238966f138edee58 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 21:17:10 +0000 Subject: [PATCH 10/12] Fix test expected values based on actual decoder behavior Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com> --- .../System.Memory/tests/Base64/Base64DecoderUnitTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs index ab2c9286227ecc..da60074d10d2e6 100644 --- a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs @@ -929,9 +929,9 @@ public void DecodingPaddedQuantumWithIsFinalBlockFalse() } [Theory] - [InlineData("AQIDBAUG AQ\r\nQ=", 10, 6, "AQ\r\nQ=")] // Two complete blocks, then whitespace-split final quantum - [InlineData("AQID BAUG AQ\r\nQ=", 11, 6, "AQ\r\nQ=")] // Two blocks with space, then whitespace-split final quantum - [InlineData("AQIDBAUG\r\nAQID AQ\r\nQ=", 17, 9, "AQ\r\nQ=")] // Multiple blocks with various whitespace patterns + [InlineData("AQIDBAUG AQ\r\nQ=", 9, 6, " AQ\r\nQ=")] // Two complete blocks, then whitespace-split final quantum + [InlineData("AQID BAUG AQ\r\nQ=", 10, 6, " AQ\r\nQ=")] // Two blocks with space, then whitespace-split final quantum + [InlineData("AQIDBAUG\r\nAQID AQ\r\nQ=", 15, 9, " AQ\r\nQ=")] // Multiple blocks with various whitespace patterns public void DecodingWithValidDataBeforeWhiteSpaceSplitFinalQuantum(string base64String, int expectedBytesConsumedFirstCall, int expectedBytesWrittenFirstCall, string expectedRemainingAfterFirstCall) { // When there's valid data before a whitespace-split final quantum and isFinalBlock=false, From 694e23f574946060a7baf13cf31f0d631e35d254 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Wed, 21 Jan 2026 14:34:39 +0100 Subject: [PATCH 11/12] Fix the test --- .../System.Memory/tests/Base64/Base64DecoderUnitTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs index cf3418dbdf5a0d..81ea7ba0b9630b 100644 --- a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs @@ -1057,9 +1057,9 @@ public void DecodingPaddedQuantumWithIsFinalBlockFalse() } [Theory] - [InlineData("AQIDBAUG AQ\r\nQ=", 9, 6, " AQ\r\nQ=")] // Two complete blocks, then whitespace-split final quantum - [InlineData("AQID BAUG AQ\r\nQ=", 10, 6, " AQ\r\nQ=")] // Two blocks with space, then whitespace-split final quantum - [InlineData("AQIDBAUG\r\nAQID AQ\r\nQ=", 15, 9, " AQ\r\nQ=")] // Multiple blocks with various whitespace patterns + [InlineData("AQIDBAUG AQ\r\nQ=", 9, 6, "AQ\r\nQ=")] // Two complete blocks, then whitespace-split final quantum + [InlineData("AQID BAUG AQ\r\nQ=", 10, 6, "AQ\r\nQ=")] // Two blocks with space, then whitespace-split final quantum + [InlineData("AQIDBAUG\r\nAQID AQ\r\nQ=", 15, 9, "AQ\r\nQ=")] // Multiple blocks with various whitespace patterns public void DecodingWithValidDataBeforeWhiteSpaceSplitFinalQuantum(string base64String, int expectedBytesConsumedFirstCall, int expectedBytesWrittenFirstCall, string expectedRemainingAfterFirstCall) { // When there's valid data before a whitespace-split final quantum and isFinalBlock=false, From 00fd4f53920cb61a6de8e52a27c5fd9d2818bfec Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Wed, 21 Jan 2026 15:19:20 +0100 Subject: [PATCH 12/12] Complete the fix --- .../tests/Base64/Base64DecoderUnitTests.cs | 89 ++++++++++++++++++- .../Text/Base64Helper/Base64DecoderHelper.cs | 39 +++++--- 2 files changed, 111 insertions(+), 17 deletions(-) diff --git a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs index 81ea7ba0b9630b..e3b6635d3e4e89 100644 --- a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs @@ -1069,16 +1069,16 @@ public void DecodingWithValidDataBeforeWhiteSpaceSplitFinalQuantum(string base64 // First call with isFinalBlock=false should decode the valid complete blocks and stop before the incomplete final quantum OperationStatus status = Base64.DecodeFromUtf8(base64Data, output, out int bytesConsumed, out int bytesWritten, isFinalBlock: false); - + Assert.Equal(OperationStatus.InvalidData, status); Assert.Equal(expectedBytesConsumedFirstCall, bytesConsumed); Assert.Equal(expectedBytesWrittenFirstCall, bytesWritten); - + // Verify that only the final block remains ReadOnlySpan remaining = base64Data.Slice(bytesConsumed); string remainingString = Encoding.ASCII.GetString(remaining); Assert.Equal(expectedRemainingAfterFirstCall, remainingString); - + // Verify we can complete decoding by retrying with the FULL input and isFinalBlock=true Array.Clear(output, 0, output.Length); status = Base64.DecodeFromUtf8(base64Data, output, out bytesConsumed, out bytesWritten, isFinalBlock: true); @@ -1103,5 +1103,88 @@ public void DecodingWithEmbeddedWhiteSpaceIntoSmallDestination_TrailingWhiteSpac Assert.Equal(destination.Length, written); Assert.Equal(new byte[] { 240, 159, 141, 137, 240, 159 }, destination); } + + [Theory] + [InlineData("AQ\r\nQ=")] + [InlineData("AQ\r\nQ=\r\n")] + [InlineData("AQ Q=")] + [InlineData("AQ\tQ=")] + public void DecodingFromCharsWithWhiteSpaceSplitFinalQuantumAndIsFinalBlockFalse(string base64String) + { + // When a final quantum (containing padding) is split by whitespace and isFinalBlock=false, + // the decoder should not consume any bytes, allowing the caller to retry with isFinalBlock=true + ReadOnlySpan base64Data = base64String.AsSpan(); + var output = new byte[10]; + + // First call with isFinalBlock=false should consume 0 bytes + OperationStatus status = Base64.DecodeFromChars(base64Data, output, out int bytesConsumed, out int bytesWritten, isFinalBlock: false); + Assert.Equal(0, bytesConsumed); + Assert.Equal(0, bytesWritten); + Assert.Equal(OperationStatus.InvalidData, status); + + // Second call with isFinalBlock=true should succeed + status = Base64.DecodeFromChars(base64Data, output, out bytesConsumed, out bytesWritten, isFinalBlock: true); + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(base64Data.Length, bytesConsumed); + Assert.Equal(2, bytesWritten); // "AQQ=" decodes to 2 bytes: {1, 4} + Assert.Equal(new byte[] { 1, 4 }, output[..2]); + } + + [Fact] + public void DecodingFromCharsCompleteQuantumWithIsFinalBlockFalse() + { + // Complete quantum without padding should be decoded even when isFinalBlock=false + ReadOnlySpan base64Data = "AAAA".AsSpan(); + var output = new byte[10]; + + OperationStatus status = Base64.DecodeFromChars(base64Data, output, out int bytesConsumed, out int bytesWritten, isFinalBlock: false); + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(4, bytesConsumed); + Assert.Equal(3, bytesWritten); + } + + [Fact] + public void DecodingFromCharsPaddedQuantumWithIsFinalBlockFalse() + { + // Quantum with padding should not be decoded when isFinalBlock=false + ReadOnlySpan base64Data = "AAA=".AsSpan(); + var output = new byte[10]; + + OperationStatus status = Base64.DecodeFromChars(base64Data, output, out int bytesConsumed, out int bytesWritten, isFinalBlock: false); + Assert.Equal(OperationStatus.InvalidData, status); + Assert.Equal(0, bytesConsumed); + Assert.Equal(0, bytesWritten); + } + + [Theory] + [InlineData("AQIDBAUG AQ\r\nQ=", 9, 6, "AQ\r\nQ=")] // Two complete blocks, then whitespace-split final quantum + [InlineData("AQID BAUG AQ\r\nQ=", 10, 6, "AQ\r\nQ=")] // Two blocks with space, then whitespace-split final quantum + [InlineData("AQIDBAUG\r\nAQID AQ\r\nQ=", 15, 9, "AQ\r\nQ=")] // Multiple blocks with various whitespace patterns + public void DecodingFromCharsWithValidDataBeforeWhiteSpaceSplitFinalQuantum(string base64String, int expectedBytesConsumedFirstCall, int expectedBytesWrittenFirstCall, string expectedRemainingAfterFirstCall) + { + // When there's valid data before a whitespace-split final quantum and isFinalBlock=false, + // verify the streaming scenario works correctly + ReadOnlySpan base64Data = base64String.AsSpan(); + var output = new byte[100]; + + // First call with isFinalBlock=false should decode the valid complete blocks and stop before the incomplete final quantum + OperationStatus status = Base64.DecodeFromChars(base64Data, output, out int bytesConsumed, out int bytesWritten, isFinalBlock: false); + + Assert.Equal(OperationStatus.InvalidData, status); + Assert.Equal(expectedBytesConsumedFirstCall, bytesConsumed); + Assert.Equal(expectedBytesWrittenFirstCall, bytesWritten); + + // Verify that only the final block remains + ReadOnlySpan remaining = base64Data.Slice(bytesConsumed); + string remainingString = new string(remaining); + Assert.Equal(expectedRemainingAfterFirstCall, remainingString); + + // Verify we can complete decoding by retrying with the FULL input and isFinalBlock=true + Array.Clear(output, 0, output.Length); + status = Base64.DecodeFromChars(base64Data, output, out bytesConsumed, out bytesWritten, isFinalBlock: true); + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(base64Data.Length, bytesConsumed); + Assert.True(bytesWritten > 0, "Should have decoded data"); + } } } 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 7bd5da6792f7ab..c94f8dfe9963a7 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 @@ -467,6 +467,14 @@ internal static OperationStatus DecodeWithWhiteSpaceBlockwise(TB while (!source.IsEmpty) { + // Skip over any leading whitespace + if (IsWhiteSpace(source[0])) + { + source = source.Slice(1); + bytesConsumed++; + continue; + } + int encodedIdx = 0; int bufferIdx = 0; int skipped = 0; @@ -485,12 +493,7 @@ internal static OperationStatus DecodeWithWhiteSpaceBlockwise(TB } source = source.Slice(encodedIdx); - - if (bufferIdx == 0) - { - bytesConsumed += skipped; - continue; - } + Debug.Assert(bufferIdx > 0); bool hasAnotherBlock; @@ -554,6 +557,7 @@ internal static OperationStatus DecodeWithWhiteSpaceBlockwise(TB } bytes = bytes.Slice(localWritten); + Debug.Assert(!source.IsEmpty); } return status; @@ -568,6 +572,14 @@ internal static OperationStatus DecodeWithWhiteSpaceBlockwise(TB while (!source.IsEmpty) { + // Skip over any leading whitespace + if (IsWhiteSpace(source[0])) + { + source = source.Slice(1); + bytesConsumed++; + continue; + } + int encodedIdx = 0; int bufferIdx = 0; int skipped = 0; @@ -586,12 +598,7 @@ internal static OperationStatus DecodeWithWhiteSpaceBlockwise(TB } source = source.Slice(encodedIdx); - bytesConsumed += skipped; - - if (bufferIdx == 0) - { - continue; - } + Debug.Assert(bufferIdx > 0); bool hasAnotherBlock; @@ -623,14 +630,17 @@ internal static OperationStatus DecodeWithWhiteSpaceBlockwise(TB } status = DecodeFrom(decoder, buffer.Slice(0, bufferIdx), bytes, out int localConsumed, out int localWritten, localIsFinalBlock, ignoreWhiteSpace: false); - bytesConsumed += localConsumed; - bytesWritten += localWritten; if (status != OperationStatus.Done) { + Debug.Assert(localConsumed == 0 && localWritten == 0, "On failure, should not have consumed or written any bytes"); return status; } + bytesConsumed += skipped; + bytesConsumed += localConsumed; + bytesWritten += localWritten; + // The remaining data must all be whitespace in order to be valid. if (!hasAnotherBlock) { @@ -651,6 +661,7 @@ internal static OperationStatus DecodeWithWhiteSpaceBlockwise(TB } bytes = bytes.Slice(localWritten); + Debug.Assert(!source.IsEmpty); } return status;