Skip to content

Commit db2f389

Browse files
CopilotstephentoubMihaZupan
authored
Add Base64 parity APIs with Base64Url (#123151)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com> Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>
1 parent 0c56d0e commit db2f389

File tree

15 files changed

+1390
-955
lines changed

15 files changed

+1390
-955
lines changed

src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/Base64Fuzzer.cs

Lines changed: 163 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ public void FuzzTarget(ReadOnlySpan<byte> bytes)
2424
}
2525

2626
private void TestCases(Span<byte> input, PoisonPagePlacement poison)
27-
{
27+
{
2828
TestBase64(input, poison);
29+
TestBase64Chars(input, poison);
2930
TestToStringToCharArray(input, Base64FormattingOptions.None);
3031
TestToStringToCharArray(input, Base64FormattingOptions.InsertLineBreaks);
3132
}
@@ -129,6 +130,167 @@ private void TestBase64(Span<byte> input, PoisonPagePlacement poison)
129130
Assert.Equal(OperationStatus.InvalidData, Base64.DecodeFromUtf8InPlace(input, out int inPlaceDecoded));
130131
}
131132
}
133+
134+
{ // Test new simplified UTF-8 APIs
135+
// Test EncodeToUtf8 returning byte[]
136+
byte[] encodedArray = Base64.EncodeToUtf8(input);
137+
Assert.Equal(true, maxEncodedLength >= encodedArray.Length && maxEncodedLength - 2 <= encodedArray.Length);
138+
139+
// Test EncodeToUtf8 returning int
140+
encoderDest.Clear();
141+
int charsWritten = Base64.EncodeToUtf8(input, encoderDest);
142+
Assert.SequenceEqual(encodedArray.AsSpan(), encoderDest.Slice(0, charsWritten));
143+
144+
// Test TryEncodeToUtf8
145+
encoderDest.Clear();
146+
Assert.Equal(true, Base64.TryEncodeToUtf8(input, encoderDest, out int tryCharsWritten));
147+
Assert.Equal(charsWritten, tryCharsWritten);
148+
Assert.SequenceEqual(encodedArray.AsSpan(), encoderDest.Slice(0, tryCharsWritten));
149+
150+
// Test DecodeFromUtf8 returning byte[]
151+
byte[] decodedArray = Base64.DecodeFromUtf8(encodedArray);
152+
Assert.SequenceEqual(input, decodedArray.AsSpan());
153+
154+
// Test DecodeFromUtf8 returning int
155+
decoderDest.Clear();
156+
int bytesWritten = Base64.DecodeFromUtf8(encodedArray, decoderDest);
157+
Assert.Equal(input.Length, bytesWritten);
158+
Assert.SequenceEqual(input, decoderDest.Slice(0, bytesWritten));
159+
160+
// Test TryDecodeFromUtf8
161+
decoderDest.Clear();
162+
Assert.Equal(true, Base64.TryDecodeFromUtf8(encodedArray, decoderDest, out int tryBytesWritten));
163+
Assert.Equal(input.Length, tryBytesWritten);
164+
Assert.SequenceEqual(input, decoderDest.Slice(0, tryBytesWritten));
165+
166+
// Test TryEncodeToUtf8InPlace
167+
using PooledBoundedMemory<byte> inPlaceBuffer = PooledBoundedMemory<byte>.Rent(maxEncodedLength, poison);
168+
Span<byte> inPlaceDest = inPlaceBuffer.Span;
169+
input.CopyTo(inPlaceDest);
170+
Assert.Equal(true, Base64.TryEncodeToUtf8InPlace(inPlaceDest, input.Length, out int inPlaceWritten));
171+
Assert.SequenceEqual(encodedArray.AsSpan(), inPlaceDest.Slice(0, inPlaceWritten));
172+
173+
// Test GetEncodedLength matches GetMaxEncodedToUtf8Length
174+
Assert.Equal(Base64.GetMaxEncodedToUtf8Length(input.Length), Base64.GetEncodedLength(input.Length));
175+
176+
// Test GetMaxDecodedLength matches GetMaxDecodedFromUtf8Length
177+
Assert.Equal(Base64.GetMaxDecodedFromUtf8Length(maxEncodedLength), Base64.GetMaxDecodedLength(maxEncodedLength));
178+
}
179+
}
180+
181+
private static void TestBase64Chars(Span<byte> input, PoisonPagePlacement poison)
182+
{
183+
int encodedLength = Base64.GetEncodedLength(input.Length);
184+
int maxDecodedLength = Base64.GetMaxDecodedLength(encodedLength);
185+
186+
using PooledBoundedMemory<char> destPoisoned = PooledBoundedMemory<char>.Rent(encodedLength, poison);
187+
using PooledBoundedMemory<byte> decoderDestPoisoned = PooledBoundedMemory<byte>.Rent(maxDecodedLength, poison);
188+
189+
Span<char> encoderDest = destPoisoned.Span;
190+
Span<byte> decoderDest = decoderDestPoisoned.Span;
191+
192+
{ // IsFinalBlock = true
193+
OperationStatus status = Base64.EncodeToChars(input, encoderDest, out int bytesConsumed, out int charsEncoded);
194+
195+
Assert.Equal(OperationStatus.Done, status);
196+
Assert.Equal(input.Length, bytesConsumed);
197+
Assert.Equal(encodedLength, charsEncoded);
198+
199+
string encodedString = Base64.EncodeToString(input);
200+
Assert.Equal(encodedString, new string(encoderDest.Slice(0, charsEncoded)));
201+
202+
status = Base64.DecodeFromChars(encoderDest.Slice(0, charsEncoded), decoderDest, out int charsRead, out int bytesDecoded);
203+
204+
Assert.Equal(OperationStatus.Done, status);
205+
Assert.Equal(input.Length, bytesDecoded);
206+
Assert.Equal(charsEncoded, charsRead);
207+
Assert.SequenceEqual(input, decoderDest.Slice(0, bytesDecoded));
208+
}
209+
210+
{ // IsFinalBlock = false
211+
encoderDest.Clear();
212+
decoderDest.Clear();
213+
OperationStatus status = Base64.EncodeToChars(input, encoderDest, out int bytesConsumed, out int charsEncoded, isFinalBlock: false);
214+
Span<char> decodeInput = encoderDest.Slice(0, charsEncoded);
215+
216+
if (input.Length % 3 == 0)
217+
{
218+
Assert.Equal(OperationStatus.Done, status);
219+
Assert.Equal(input.Length, bytesConsumed);
220+
Assert.Equal(encodedLength, charsEncoded);
221+
222+
status = Base64.DecodeFromChars(decodeInput, decoderDest, out int charsRead, out int bytesDecoded, isFinalBlock: false);
223+
224+
Assert.Equal(OperationStatus.Done, status);
225+
Assert.Equal(input.Length, bytesDecoded);
226+
Assert.Equal(charsEncoded, charsRead);
227+
Assert.SequenceEqual(input, decoderDest.Slice(0, bytesDecoded));
228+
}
229+
else
230+
{
231+
Assert.Equal(OperationStatus.NeedMoreData, status);
232+
Assert.Equal(true, input.Length / 3 * 4 == charsEncoded);
233+
234+
status = Base64.DecodeFromChars(decodeInput, decoderDest, out int charsRead, out int bytesDecoded, isFinalBlock: false);
235+
236+
if (decodeInput.Length % 4 == 0)
237+
{
238+
Assert.Equal(OperationStatus.Done, status);
239+
Assert.Equal(bytesConsumed, bytesDecoded);
240+
Assert.Equal(charsEncoded, charsRead);
241+
}
242+
else
243+
{
244+
Assert.Equal(OperationStatus.NeedMoreData, status);
245+
}
246+
247+
Assert.SequenceEqual(input.Slice(0, bytesDecoded), decoderDest.Slice(0, bytesDecoded));
248+
}
249+
}
250+
251+
{ // Test array-returning and int-returning overloads
252+
char[] encodedChars = Base64.EncodeToChars(input);
253+
Assert.Equal(encodedLength, encodedChars.Length);
254+
255+
encoderDest.Clear();
256+
int charsWritten = Base64.EncodeToChars(input, encoderDest);
257+
Assert.Equal(encodedLength, charsWritten);
258+
Assert.SequenceEqual(encodedChars.AsSpan(), encoderDest.Slice(0, charsWritten));
259+
260+
byte[] decodedBytes = Base64.DecodeFromChars(encodedChars);
261+
Assert.SequenceEqual(input, decodedBytes.AsSpan());
262+
263+
decoderDest.Clear();
264+
int bytesWritten = Base64.DecodeFromChars(encodedChars, decoderDest);
265+
Assert.Equal(input.Length, bytesWritten);
266+
Assert.SequenceEqual(input, decoderDest.Slice(0, bytesWritten));
267+
}
268+
269+
{ // Test Try* variants
270+
encoderDest.Clear();
271+
Assert.Equal(true, Base64.TryEncodeToChars(input, encoderDest, out int charsWritten));
272+
Assert.Equal(encodedLength, charsWritten);
273+
274+
decoderDest.Clear();
275+
Assert.Equal(true, Base64.TryDecodeFromChars(encoderDest.Slice(0, charsWritten), decoderDest, out int bytesWritten));
276+
Assert.Equal(input.Length, bytesWritten);
277+
Assert.SequenceEqual(input, decoderDest.Slice(0, bytesWritten));
278+
}
279+
280+
{ // Decode the random chars directly (as chars, from the input bytes interpreted as UTF-16)
281+
// Create a char span from the input bytes for testing decode with random data
282+
if (input.Length >= 2)
283+
{
284+
ReadOnlySpan<char> inputChars = System.Runtime.InteropServices.MemoryMarshal.Cast<byte, char>(input);
285+
decoderDest.Clear();
286+
287+
// Try decoding - may succeed or fail depending on if input is valid base64
288+
OperationStatus status = Base64.DecodeFromChars(inputChars, decoderDest, out int charsConsumed, out int bytesDecoded);
289+
// Just verify we don't crash - the result depends on input validity
290+
Assert.Equal(true, status == OperationStatus.Done || status == OperationStatus.InvalidData ||
291+
status == OperationStatus.NeedMoreData || status == OperationStatus.DestinationTooSmall);
292+
}
293+
}
132294
}
133295

134296
private static void TestToStringToCharArray(Span<byte> input, Base64FormattingOptions options)

src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,134 @@ public void BasicDecodingWithExtraWhitespaceShouldBeCountedInConsumedBytes(strin
771771
Assert.True(Base64TestHelper.VerifyDecodingCorrectness(expectedConsumed, expectedWritten, source, decodedBytes));
772772
}
773773

774+
[Fact]
775+
public void DecodeFromCharsWithLargeSpan()
776+
{
777+
var rnd = new Random(42);
778+
for (int i = 0; i < 5; i++)
779+
{
780+
int numBytes = rnd.Next(100, 1000 * 1000);
781+
// Ensure we have a valid length (multiple of 4 for standard Base64)
782+
numBytes = (numBytes / 4) * 4;
783+
784+
Span<char> source = new char[numBytes];
785+
Base64TestHelper.InitializeDecodableChars(source, numBytes);
786+
787+
Span<byte> decodedBytes = new byte[Base64.GetMaxDecodedLength(source.Length)];
788+
Assert.Equal(OperationStatus.Done, Base64.DecodeFromChars(source, decodedBytes, out int consumed, out int decodedByteCount));
789+
Assert.Equal(source.Length, consumed);
790+
791+
string sourceString = source.ToString();
792+
byte[] expectedBytes = Convert.FromBase64String(sourceString);
793+
Assert.True(expectedBytes.AsSpan().SequenceEqual(decodedBytes.Slice(0, decodedByteCount)));
794+
}
795+
}
796+
797+
[Theory]
798+
[InlineData("\u5948cz/T", 0, 0)] // tests the scalar code-path with non-ASCII
799+
[InlineData("z/Ta123\u5948", 4, 3)]
800+
public void DecodeFromCharsNonAsciiInputInvalid(string inputString, int expectedConsumed, int expectedWritten)
801+
{
802+
Span<char> source = inputString.ToArray();
803+
Span<byte> decodedBytes = new byte[Base64.GetMaxDecodedLength(source.Length)];
804+
805+
Assert.Equal(OperationStatus.InvalidData, Base64.DecodeFromChars(source, decodedBytes, out int consumed, out int decodedByteCount));
806+
Assert.Equal(expectedConsumed, consumed);
807+
Assert.Equal(expectedWritten, decodedByteCount);
808+
}
809+
810+
[Fact]
811+
public void DecodeFromUtf8_ArrayOverload()
812+
{
813+
byte[] utf8Input = Encoding.UTF8.GetBytes("dGVzdA=="); // "test" encoded
814+
byte[] result = Base64.DecodeFromUtf8(utf8Input);
815+
Assert.Equal(4, result.Length);
816+
Assert.Equal("test", Encoding.UTF8.GetString(result));
817+
}
818+
819+
[Fact]
820+
public void DecodeFromUtf8_SpanOverload()
821+
{
822+
byte[] utf8Input = Encoding.UTF8.GetBytes("dGVzdA=="); // "test" encoded
823+
Span<byte> destination = new byte[10];
824+
int bytesWritten = Base64.DecodeFromUtf8(utf8Input, destination);
825+
Assert.Equal(4, bytesWritten);
826+
Assert.Equal("test", Encoding.UTF8.GetString(destination.Slice(0, bytesWritten)));
827+
}
828+
829+
[Fact]
830+
public void TryDecodeFromUtf8_Success()
831+
{
832+
byte[] utf8Input = Encoding.UTF8.GetBytes("dGVzdA==");
833+
Span<byte> destination = new byte[10];
834+
Assert.True(Base64.TryDecodeFromUtf8(utf8Input, destination, out int bytesWritten));
835+
Assert.Equal(4, bytesWritten);
836+
Assert.Equal("test", Encoding.UTF8.GetString(destination.Slice(0, bytesWritten)));
837+
}
838+
839+
[Fact]
840+
public void TryDecodeFromUtf8_DestinationTooSmall()
841+
{
842+
byte[] utf8Input = Encoding.UTF8.GetBytes("dGVzdA==");
843+
Span<byte> destination = new byte[2]; // Too small
844+
Assert.False(Base64.TryDecodeFromUtf8(utf8Input, destination, out int bytesWritten));
845+
Assert.Equal(0, bytesWritten);
846+
}
847+
848+
[Fact]
849+
public void DecodeFromChars_InvalidData()
850+
{
851+
string invalidInput = "@#$%";
852+
byte[] destination = new byte[10];
853+
Assert.Throws<FormatException>(() => Base64.DecodeFromChars(invalidInput, destination));
854+
Assert.Throws<FormatException>(() => Base64.DecodeFromChars(invalidInput.AsSpan()));
855+
}
856+
857+
[Fact]
858+
public void DecodeFromChars_DestinationTooSmall()
859+
{
860+
string validInput = "dGVzdA=="; // "test" encoded
861+
byte[] destination = new byte[2]; // Too small
862+
Assert.Throws<ArgumentException>("destination", () => Base64.DecodeFromChars(validInput, destination));
863+
}
864+
865+
[Fact]
866+
public void TryDecodeFromChars_DestinationTooSmall()
867+
{
868+
string validInput = "dGVzdA=="; // "test" encoded
869+
Span<byte> destination = new byte[2]; // Too small
870+
Assert.False(Base64.TryDecodeFromChars(validInput, destination, out int bytesWritten));
871+
}
872+
873+
[Fact]
874+
public void DecodeFromChars_OperationStatus_DistinguishesBetweenInvalidAndDestinationTooSmall()
875+
{
876+
// This is the key use case from the issue - distinguishing between invalid data and destination too small
877+
string validInput = "dGVzdA=="; // "test" encoded - produces 4 bytes
878+
string invalidInput = "@#$%";
879+
Span<byte> smallDestination = new byte[2];
880+
881+
// With destination too small, we should get DestinationTooSmall
882+
OperationStatus status1 = Base64.DecodeFromChars(validInput, smallDestination, out int consumed1, out int written1);
883+
Assert.Equal(OperationStatus.DestinationTooSmall, status1);
884+
Assert.True(consumed1 > 0 || written1 >= 0); // Some progress was made or at least we know why it failed
885+
886+
// With invalid data, we should get InvalidData
887+
OperationStatus status2 = Base64.DecodeFromChars(invalidInput, smallDestination, out int consumed2, out int written2);
888+
Assert.Equal(OperationStatus.InvalidData, status2);
889+
Assert.Equal(0, consumed2);
890+
Assert.Equal(0, written2);
891+
}
892+
893+
[Fact]
894+
public void GetMaxDecodedLength_Matches_GetMaxDecodedFromUtf8Length()
895+
{
896+
for (int i = 0; i < 100; i++)
897+
{
898+
Assert.Equal(Base64.GetMaxDecodedFromUtf8Length(i), Base64.GetMaxDecodedLength(i));
899+
}
900+
}
901+
774902
[Fact]
775903
public void DecodingWithWhiteSpaceIntoSmallDestination()
776904
{

0 commit comments

Comments
 (0)