Skip to content

Commit 615d641

Browse files
committed
Replace base 64 implementation with workaround
1 parent b9db5ba commit 615d641

14 files changed

+589
-23
lines changed

src/Components/Components/src/Microsoft.AspNetCore.Components.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@
88
<Nullable>enable</Nullable>
99
<IsTrimmable>true</IsTrimmable>
1010
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
11-
<DefineConstants>$(DefineConstants);COMPONENTS</DefineConstants>
11+
<DefineConstants>$(DefineConstants);COMPONENTS;USE_WORKAROUND</DefineConstants>
1212
<!-- TODO: Address Native AOT analyzer warnings https://github.com/dotnet/aspnetcore/issues/45473 -->
1313
<EnableAOTAnalyzer>false</EnableAOTAnalyzer>
1414
</PropertyGroup>
1515

1616
<ItemGroup>
1717
<Compile Include="$(ComponentsSharedSourceRoot)src\ArrayBuilder.cs" LinkBase="RenderTree" />
1818
<Compile Include="$(ComponentsSharedSourceRoot)src\JsonSerializerOptionsProvider.cs" />
19+
<Compile Include="$(ComponentsSharedSourceRoot)src\ComponentsBase64Helper.cs" LinkBase="Shared" />
1920
<Compile Include="$(ComponentsSharedSourceRoot)src\HotReloadManager.cs" LinkBase="HotReload" />
2021
<Compile Include="$(ComponentsSharedSourceRoot)src\RootTypeCache.cs" LinkBase="Shared" />
2122
<Compile Include="$(SharedSourceRoot)LinkerFlags.cs" LinkBase="Shared" />

src/Components/Components/src/PersistentState/PersistentServicesRegistry.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ private static string ComputeKey(Type keyType, string propertyName)
229229

230230
var input = Encoding.UTF8.GetBytes(inputString);
231231
var hash = SHA256.HashData(input);
232-
return Convert.ToBase64String(hash);
232+
return ComponentsBase64Helper.ToBase64(hash);
233233
}
234234

235235
internal static IEnumerable<PropertyInfo> GetCandidateBindableProperties(

src/Components/Components/src/PersistentState/PersistentStateValueProviderKeyResolver.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ private static string ComputeFinalKey(byte[] preKey, ComponentState componentSta
113113

114114
var hashSucceeded = SHA256.TryHashData(keyBuffer, keyHash, out _);
115115
Debug.Assert(hashSucceeded);
116-
return Convert.ToBase64String(keyHash);
116+
return ComponentsBase64Helper.ToBase64(keyHash);
117117
}
118118
finally
119119
{
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Text;
6+
using Xunit;
7+
8+
namespace Microsoft.AspNetCore.Components.Endpoints;
9+
10+
public class ComponentsBase64HelperTest
11+
{
12+
[Theory]
13+
[InlineData("", "")]
14+
[InlineData("f", "Zg")]
15+
[InlineData("fo", "Zm8")]
16+
[InlineData("foo", "Zm9v")]
17+
[InlineData("foob", "Zm9vYg")]
18+
[InlineData("fooba", "Zm9vYmE")]
19+
[InlineData("foobar", "Zm9vYmFy")]
20+
[InlineData("Hello, World!", "SGVsbG8sIFdvcmxkIQ")]
21+
[InlineData("The quick brown fox jumps over the lazy dog", "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw")]
22+
public void Base64UrlEncode_ProducesExpectedOutput(string input, string expectedBase64Url)
23+
{
24+
// Arrange
25+
var inputBytes = Encoding.UTF8.GetBytes(input);
26+
27+
// Act
28+
var actualBase64Url = Microsoft.AspNetCore.Components.ComponentsBase64Helper.Base64UrlEncode(inputBytes);
29+
30+
// Assert
31+
Assert.Equal(expectedBase64Url, actualBase64Url);
32+
}
33+
34+
[Theory]
35+
[InlineData("", "")]
36+
[InlineData("Zg", "f")]
37+
[InlineData("Zm8", "fo")]
38+
[InlineData("Zm9v", "foo")]
39+
[InlineData("Zm9vYg", "foob")]
40+
[InlineData("Zm9vYmE", "fooba")]
41+
[InlineData("Zm9vYmFy", "foobar")]
42+
[InlineData("SGVsbG8sIFdvcmxkIQ", "Hello, World!")]
43+
[InlineData("VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw", "The quick brown fox jumps over the lazy dog")]
44+
public void Base64UrlDecode_ProducesExpectedOutput(string base64UrlInput, string expectedOutput)
45+
{
46+
// Act
47+
var actualBytes = Microsoft.AspNetCore.Components.ComponentsBase64Helper.Base64UrlDecode(base64UrlInput);
48+
var actualOutput = Encoding.UTF8.GetString(actualBytes);
49+
50+
// Assert
51+
Assert.Equal(expectedOutput, actualOutput);
52+
}
53+
54+
[Theory]
55+
[InlineData("", "")]
56+
[InlineData("f", "Zg")]
57+
[InlineData("fo", "Zm8")]
58+
[InlineData("foo", "Zm9v")]
59+
[InlineData("foob", "Zm9vYg")]
60+
[InlineData("fooba", "Zm9vYmE")]
61+
[InlineData("foobar", "Zm9vYmFy")]
62+
public void ToBase64Url_WithReadOnlySpan_ProducesExpectedOutput(string input, string expectedBase64Url)
63+
{
64+
// Arrange
65+
var inputBytes = Encoding.UTF8.GetBytes(input);
66+
67+
// Act
68+
var actualBase64Url = Microsoft.AspNetCore.Components.ComponentsBase64Helper.ToBase64Url((ReadOnlySpan<byte>)inputBytes);
69+
70+
// Assert
71+
Assert.Equal(expectedBase64Url, actualBase64Url);
72+
}
73+
74+
[Theory]
75+
[InlineData("", "")]
76+
[InlineData("f", "Zg")]
77+
[InlineData("fo", "Zm8")]
78+
[InlineData("foo", "Zm9v")]
79+
[InlineData("foob", "Zm9vYg")]
80+
[InlineData("fooba", "Zm9vYmE")]
81+
[InlineData("foobar", "Zm9vYmFy")]
82+
public void ToBase64Url_WithSpan_ProducesExpectedOutput(string input, string expectedBase64Url)
83+
{
84+
// Arrange
85+
var inputBytes = Encoding.UTF8.GetBytes(input);
86+
87+
// Act
88+
var actualBase64Url = Microsoft.AspNetCore.Components.ComponentsBase64Helper.ToBase64Url((Span<byte>)inputBytes);
89+
90+
// Assert
91+
Assert.Equal(expectedBase64Url, actualBase64Url);
92+
}
93+
94+
[Theory]
95+
[InlineData("", 0)]
96+
[InlineData("f", 2)]
97+
[InlineData("fo", 3)]
98+
[InlineData("foo", 4)]
99+
[InlineData("foob", 6)]
100+
[InlineData("fooba", 7)]
101+
[InlineData("foobar", 8)]
102+
public void ToBase64Url_WithSpanOutput_ProducesExpectedOutput(string input, int expectedLength)
103+
{
104+
// Arrange
105+
var inputBytes = Encoding.UTF8.GetBytes(input);
106+
Span<char> output = stackalloc char[20]; // Sufficient space
107+
108+
// Act
109+
var actualLength = Microsoft.AspNetCore.Components.ComponentsBase64Helper.ToBase64Url(inputBytes, output);
110+
111+
// Assert
112+
Assert.Equal(expectedLength, actualLength);
113+
114+
if (expectedLength > 0)
115+
{
116+
var actualOutput = output[..actualLength].ToString();
117+
var expectedOutput = Microsoft.AspNetCore.Components.ComponentsBase64Helper.ToBase64Url(inputBytes);
118+
Assert.Equal(expectedOutput, actualOutput);
119+
}
120+
}
121+
122+
[Fact]
123+
public void Base64UrlEncode_WithNullInput_ReturnsEmptyString()
124+
{
125+
// Act
126+
var result = Microsoft.AspNetCore.Components.ComponentsBase64Helper.Base64UrlEncode(null);
127+
128+
// Assert
129+
Assert.Equal(string.Empty, result);
130+
}
131+
132+
[Fact]
133+
public void Base64UrlDecode_WithNullInput_ReturnsEmptyArray()
134+
{
135+
// Act
136+
var result = Microsoft.AspNetCore.Components.ComponentsBase64Helper.Base64UrlDecode(null);
137+
138+
// Assert
139+
Assert.Empty(result);
140+
}
141+
142+
[Fact]
143+
public void Base64UrlDecode_WithEmptyString_ReturnsEmptyArray()
144+
{
145+
// Act
146+
var result = Microsoft.AspNetCore.Components.ComponentsBase64Helper.Base64UrlDecode("");
147+
148+
// Assert
149+
Assert.Empty(result);
150+
}
151+
152+
[Theory]
153+
[InlineData("any carnal pleasure.", "YW55IGNhcm5hbCBwbGVhc3VyZS4")]
154+
[InlineData("any carnal pleasure", "YW55IGNhcm5hbCBwbGVhc3VyZQ")]
155+
[InlineData("any carnal pleasur", "YW55IGNhcm5hbCBwbGVhc3Vy")]
156+
[InlineData("any carnal pleasu", "YW55IGNhcm5hbCBwbGVhc3U")]
157+
[InlineData("any carnal pleas", "YW55IGNhcm5hbCBwbGVhcw")]
158+
public void Base64UrlEncodeAndDecode_RoundTrip_PreservesOriginalData(string originalText, string expectedBase64Url)
159+
{
160+
// Arrange
161+
var originalBytes = Encoding.UTF8.GetBytes(originalText);
162+
163+
// Act - Encode
164+
var encoded = Microsoft.AspNetCore.Components.ComponentsBase64Helper.Base64UrlEncode(originalBytes);
165+
166+
// Assert - Check encoded value
167+
Assert.Equal(expectedBase64Url, encoded);
168+
169+
// Act - Decode
170+
var decodedBytes = Microsoft.AspNetCore.Components.ComponentsBase64Helper.Base64UrlDecode(encoded);
171+
var decodedText = Encoding.UTF8.GetString(decodedBytes);
172+
173+
// Assert - Check round-trip
174+
Assert.Equal(originalText, decodedText);
175+
Assert.Equal(originalBytes, decodedBytes);
176+
}
177+
178+
[Theory]
179+
[InlineData(new byte[] { 0x00 }, "AA")]
180+
[InlineData(new byte[] { 0xFF }, "_w")]
181+
[InlineData(new byte[] { 0x00, 0xFF }, "AP8")]
182+
[InlineData(new byte[] { 0xFF, 0x00 }, "_wA")]
183+
[InlineData(new byte[] { 0x3E, 0x3F }, "Pj8")]
184+
[InlineData(new byte[] { 0xFC, 0xFD, 0xFE, 0xFF }, "_P3-_w")]
185+
public void Base64UrlEncode_WithSpecialBytes_ProducesCorrectUrlSafeOutput(byte[] input, string expectedOutput)
186+
{
187+
// Act
188+
var actual = Microsoft.AspNetCore.Components.ComponentsBase64Helper.Base64UrlEncode(input);
189+
190+
// Assert
191+
Assert.Equal(expectedOutput, actual);
192+
193+
// Verify it doesn't contain URL-unsafe characters
194+
Assert.DoesNotContain('+', actual);
195+
Assert.DoesNotContain('/', actual);
196+
Assert.DoesNotContain('=', actual);
197+
}
198+
199+
[Theory]
200+
[InlineData("AA", new byte[] { 0x00 })]
201+
[InlineData("_w", new byte[] { 0xFF })]
202+
[InlineData("AP8", new byte[] { 0x00, 0xFF })]
203+
[InlineData("_wA", new byte[] { 0xFF, 0x00 })]
204+
[InlineData("Pj8", new byte[] { 0x3E, 0x3F })]
205+
[InlineData("_P3-_w", new byte[] { 0xFC, 0xFD, 0xFE, 0xFF })]
206+
public void Base64UrlDecode_WithUrlSafeCharacters_ProducesCorrectOutput(string input, byte[] expectedOutput)
207+
{
208+
// Act
209+
var actual = Microsoft.AspNetCore.Components.ComponentsBase64Helper.Base64UrlDecode(input);
210+
211+
// Assert
212+
Assert.Equal(expectedOutput, actual);
213+
}
214+
215+
[Fact]
216+
public void ToBase64Url_WithEmptyReadOnlySpan_ReturnsEmptyString()
217+
{
218+
// Arrange
219+
ReadOnlySpan<byte> emptySpan = ReadOnlySpan<byte>.Empty;
220+
221+
// Act
222+
var result = Microsoft.AspNetCore.Components.ComponentsBase64Helper.ToBase64Url(emptySpan);
223+
224+
// Assert
225+
Assert.Equal(string.Empty, result);
226+
}
227+
228+
[Fact]
229+
public void ToBase64Url_WithEmptySpan_ReturnsEmptyString()
230+
{
231+
// Arrange
232+
Span<byte> emptySpan = Span<byte>.Empty;
233+
234+
// Act
235+
var result = Microsoft.AspNetCore.Components.ComponentsBase64Helper.ToBase64Url(emptySpan);
236+
237+
// Assert
238+
Assert.Equal(string.Empty, result);
239+
}
240+
241+
[Fact]
242+
public void ToBase64Url_WithEmptySpanOutput_ReturnsZero()
243+
{
244+
// Arrange
245+
ReadOnlySpan<byte> emptyInput = ReadOnlySpan<byte>.Empty;
246+
Span<char> output = stackalloc char[10];
247+
248+
// Act
249+
var result = Microsoft.AspNetCore.Components.ComponentsBase64Helper.ToBase64Url(emptyInput, output);
250+
251+
// Assert
252+
Assert.Equal(0, result);
253+
}
254+
255+
[Theory]
256+
[InlineData(1, 2)]
257+
[InlineData(2, 3)]
258+
[InlineData(3, 4)]
259+
[InlineData(4, 6)]
260+
[InlineData(5, 7)]
261+
[InlineData(6, 8)]
262+
public void ToBase64Url_OutputLength_IsCorrect(int inputLength, int expectedOutputLength)
263+
{
264+
// Arrange
265+
var input = new byte[inputLength];
266+
for (int i = 0; i < inputLength; i++)
267+
{
268+
input[i] = (byte)(i + 1);
269+
}
270+
271+
// Act
272+
var result = Microsoft.AspNetCore.Components.ComponentsBase64Helper.ToBase64Url(input);
273+
274+
// Assert
275+
Assert.Equal(expectedOutputLength, result.Length);
276+
}
277+
}

src/Components/Endpoints/src/Builder/ResourceCollectionUrlEndpoint.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ private static string ComputeIntegrity(byte[] bytes)
7676
{
7777
Span<byte> hash = stackalloc byte[32];
7878
SHA256.HashData(bytes, hash);
79-
return $"sha256-{Convert.ToBase64String(hash)}";
79+
return $"sha256-{ComponentsBase64Helper.ToBase64(hash)}";
8080
}
8181

8282
private static byte[] CreateGzipBytes(byte[] bytes)
@@ -123,7 +123,7 @@ private static string ComputeFingerprintSuffix(ResourceAssetCollection resourceC
123123
// Base64 encoding at most increases size by (4 * byteSize / 3 + 2),
124124
// add an extra byte for the initial dot.
125125
Span<char> fingerprintSpan = stackalloc char[(incrementalHash.HashLengthInBytes * 4 / 3) + 3];
126-
var length = WebUtilities.WebEncoders.Base64UrlEncode(result, fingerprintSpan[1..]);
126+
var length = ComponentsBase64Helper.ToBase64Url(result, fingerprintSpan[1..]);
127127
fingerprintSpan[0] = '.';
128128
return fingerprintSpan[..(length + 1)].ToString();
129129
}
@@ -176,7 +176,7 @@ private string ComputeETagTag(byte[] content)
176176
{
177177
Span<byte> data = stackalloc byte[32];
178178
SHA256.HashData(content, data);
179-
return $"\"{Convert.ToBase64String(data)}\"";
179+
return $"\"{ComponentsBase64Helper.ToBase64(data)}\"";
180180
}
181181

182182
public async Task FingerprintedGzipContent(HttpContext context)

src/Components/Endpoints/src/DependencyInjection/WebAssemblyComponentSerializer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ public static void SerializeInvocation(ref ComponentMarker marker, Type type, Pa
1616

1717
// We need to serialize and Base64 encode parameters separately since they can contain arbitrary data that might
1818
// cause the HTML comment to be invalid (like if you serialize a string that contains two consecutive dashes "--").
19-
var serializedDefinitions = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(definitions, WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
20-
var serializedValues = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(values, WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
19+
var serializedDefinitions = ComponentsBase64Helper.ToBase64(JsonSerializer.SerializeToUtf8Bytes(definitions, WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
20+
var serializedValues = ComponentsBase64Helper.ToBase64(JsonSerializer.SerializeToUtf8Bytes(values, WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
2121

2222
marker.WriteWebAssemblyData(assembly, typeFullName, serializedDefinitions, serializedValues);
2323
}

src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<NoWarn>$(NoWarn);CS0436</NoWarn>
1010
<IsPackable>false</IsPackable>
1111
<Nullable>enable</Nullable>
12-
<DefineConstants>$(DefineConstants);COMPONENTS</DefineConstants>
12+
<DefineConstants>$(DefineConstants);COMPONENTS;USE_WORKAROUND</DefineConstants>
1313
</PropertyGroup>
1414

1515
<ItemGroup>
@@ -31,6 +31,7 @@
3131
<Compile Include="$(RepoRoot)src\Shared\ClosedGenericMatcher\ClosedGenericMatcher.cs" LinkBase="FormMapping" />
3232
<Compile Include="$(ComponentsSharedSourceRoot)src\CacheHeaderSettings.cs" Link="Shared\CacheHeaderSettings.cs" />
3333
<Compile Include="$(ComponentsSharedSourceRoot)src\ResourceCollectionProvider.cs" Link="Shared\ResourceCollectionProvider.cs" />
34+
<Compile Include="$(ComponentsSharedSourceRoot)src\ComponentsBase64Helper.cs" LinkBase="Shared" />
3435
<Compile Include="$(ComponentsSharedSourceRoot)src\DefaultAntiforgeryStateProvider.cs" LinkBase="Forms" />
3536
<Compile Include="$(SharedSourceRoot)LinkerFlags.cs" LinkBase="Shared" />
3637
<Compile Include="$(SharedSourceRoot)Components\ComponentsActivityLinkStore.cs" LinkBase="Shared" />

src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ internal void EmitInitializersIfNecessary(HttpContext httpContext, TextWriter wr
9898
if (_options.JavaScriptInitializers != null &&
9999
!IsProgressivelyEnhancedNavigation(httpContext.Request))
100100
{
101-
var initializersBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(_options.JavaScriptInitializers));
101+
var initializersBase64 = ComponentsBase64Helper.ToBase64(Encoding.UTF8.GetBytes(_options.JavaScriptInitializers));
102102
writer.Write("<!--Blazor-Web-Initializers:");
103103
writer.Write(initializersBase64);
104104
writer.Write("-->");

0 commit comments

Comments
 (0)