-
Notifications
You must be signed in to change notification settings - Fork 10.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[release/8.0] [Blazor] Fix type name hashing when the type has multib…
…yte characters (#52316) Backport of #52232 to release/8.0 /cc @MackinnonBuck # [Blazor] Fix type name hashing when the type has multibyte characters Fixes an issue where an exception gets thrown if a render mode boundary component has a type whose full name contains multibyte characters. ## Description The bug results in an exception getting thrown if the type of a component with a render mode has a full name containing multibyte characters. It especially affects cases where a component (or the namespace it's defined in) contains non-Latin characters. This PR fixes the issue by allocating a constant-sized stack buffer and falling back to a heap-allocated buffer when the type name is too long. Fixes #50879 Fixes #52109 ## Customer Impact Blazor Apps with non-Latin code might not be able to use interactivity. The workaround is to ensure that the full name of any component serving as a render mode boundary does not contain multibyte characters. ## Regression? - [ ] Yes - [X] No Render modes are a new feature in .NET 8, so this bug is not a regression. ## Risk - [ ] High - [ ] Medium - [X] Low The fix is straightforward and we have new automated tests for this scenario. ## Verification - [X] Manual (required) - [X] Automated ## Packaging changes reviewed? - [ ] Yes - [ ] No - [X] N/A --------- Co-authored-by: Mackinnon Buck <[email protected]>
- Loading branch information
1 parent
6a745a4
commit 4373734
Showing
6 changed files
with
217 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Security.Cryptography; | ||
using System.Text; | ||
|
||
namespace Microsoft.AspNetCore.Components.Endpoints; | ||
|
||
// Internal for testing. | ||
internal class TypeNameHash | ||
{ | ||
public const int MaxStackBufferSize = 1024; | ||
|
||
public static string Compute(Type type) | ||
{ | ||
if (type.FullName is not { } typeName) | ||
{ | ||
throw new InvalidOperationException($"Cannot compute a hash for a type without a {nameof(Type.FullName)}."); | ||
} | ||
|
||
Span<byte> typeNameBytes = stackalloc byte[MaxStackBufferSize]; | ||
|
||
if (!Encoding.UTF8.TryGetBytes(typeName, typeNameBytes, out var written)) | ||
{ | ||
typeNameBytes = Encoding.UTF8.GetBytes(typeName); | ||
written = typeNameBytes.Length; | ||
} | ||
|
||
Span<byte> typeNameHashBytes = stackalloc byte[SHA256.HashSizeInBytes]; | ||
SHA256.HashData(typeNameBytes[..written], typeNameHashBytes); | ||
|
||
return Convert.ToHexString(typeNameHashBytes); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Security.Cryptography; | ||
|
||
namespace Microsoft.AspNetCore.Components.Endpoints; | ||
|
||
public class TypeNameHashTest | ||
{ | ||
// In these tests, we're mostly interested in checking that the hash function succeeds | ||
// for any type with a valid name. We'll also do some basic sanity checking by ensuring | ||
// that the string representation of the hash has the expected length. | ||
|
||
// We currently use a hex-encoded SHA256 hash, so there should be two characters per byte | ||
// of encoded data. | ||
private const int ExpectedHashLength = SHA256.HashSizeInBytes * 2; | ||
|
||
[Fact] | ||
public void CanComputeHashForTypeWithBasicName() | ||
{ | ||
// Act | ||
var hash = TypeNameHash.Compute(typeof(ClassWithBasicName)); | ||
|
||
// Assert | ||
Assert.Equal(ExpectedHashLength, hash.Length); | ||
} | ||
|
||
[Fact] | ||
public void CanComputeHashForTypeWithMultibyteCharacters() | ||
{ | ||
// Act | ||
var hash = TypeNameHash.Compute(typeof(ClássWïthMûltibyteÇharacters)); | ||
|
||
// Assert | ||
Assert.Equal(ExpectedHashLength, hash.Length); | ||
} | ||
|
||
[Fact] | ||
public void CanComputeHashForAnonymousType() | ||
{ | ||
// Arrange | ||
var type = new { Foo = "bar" }.GetType(); | ||
|
||
// Act | ||
var hash = TypeNameHash.Compute(type); | ||
|
||
// Assert | ||
Assert.Equal(ExpectedHashLength, hash.Length); | ||
} | ||
|
||
[Fact] | ||
public void CanComputeHashForTypeWithNameLongerThanMaxStackBufferSize() | ||
{ | ||
// Arrange | ||
// We need to use a type with a long name, so we'll use a large tuple. | ||
// We have an assert later in this test to sanity check that the type | ||
// name is indeed longer than the max stack buffer size. | ||
var type = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12).GetType(); | ||
|
||
// Act | ||
var hash = TypeNameHash.Compute(type); | ||
|
||
// Assert | ||
Assert.True(type.FullName.Length > TypeNameHash.MaxStackBufferSize); | ||
Assert.Equal(ExpectedHashLength, hash.Length); | ||
} | ||
|
||
[Fact] | ||
public void ThrowsIfTypeHasNoName() | ||
{ | ||
// Arrange | ||
var type = typeof(Nullable<>).GetGenericArguments()[0]; | ||
|
||
// Act/Assert | ||
var ex = Assert.Throws<InvalidOperationException>(() => TypeNameHash.Compute(type)); | ||
Assert.Equal($"Cannot compute a hash for a type without a {nameof(Type.FullName)}.", ex.Message); | ||
} | ||
|
||
class ClassWithBasicName; | ||
class ClássWïthMûltibyteÇharacters; | ||
} |
38 changes: 38 additions & 0 deletions
38
src/Components/test/E2ETest/ServerRenderingTests/MultibyteComponentTypeNameTest.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using Components.TestServer.RazorComponents; | ||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; | ||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; | ||
using Microsoft.AspNetCore.E2ETesting; | ||
using OpenQA.Selenium; | ||
using TestServer; | ||
using Xunit.Abstractions; | ||
|
||
namespace Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests; | ||
|
||
public class MultibyteComponentTypeNameTest : ServerTestBase<BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>>> | ||
{ | ||
public MultibyteComponentTypeNameTest( | ||
BrowserFixture browserFixture, | ||
BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>> serverFixture, | ||
ITestOutputHelper output) | ||
: base(browserFixture, serverFixture, output) | ||
{ | ||
} | ||
|
||
[Theory] | ||
[InlineData("server")] | ||
[InlineData("webassembly")] | ||
public void CanRenderInteractiveComponentsWithMultibyteName(string renderMode) | ||
{ | ||
Navigate($"{ServerPathBase}/multibyte-character-component/{renderMode}"); | ||
|
||
Browser.Equal("True", () => Browser.FindElement(By.ClassName("is-interactive")).Text); | ||
Browser.Equal("0", () => Browser.FindElement(By.ClassName("count")).Text); | ||
|
||
Browser.FindElement(By.ClassName("increment")).Click(); | ||
|
||
Browser.Equal("1", () => Browser.FindElement(By.ClassName("count")).Text); | ||
} | ||
} |
36 changes: 36 additions & 0 deletions
36
...tServer/RazorComponents/Pages/Rendering/PageRenderingComponentWithMultibyteTypeName.razor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
@page "/multibyte-character-component/{renderModeString?}" | ||
@using TestContentPackage | ||
|
||
<h1>Page rendering component with multibyte type name</h1> | ||
|
||
@if (_renderMode is null) | ||
{ | ||
<p> | ||
<b>Warning:</b> Render mode should be specified as a route parameter and have the value 'server' or 'webassembly'. | ||
</p> | ||
|
||
<p> | ||
Defaulting to a null render mode. | ||
</p> | ||
} | ||
|
||
<MûltibyteÇharacterCompoñent @rendermode="_renderMode" /> | ||
|
||
@code { | ||
private IComponentRenderMode? _renderMode; | ||
|
||
[Parameter] | ||
public string? RenderModeString { get; set; } | ||
|
||
protected override void OnInitialized() | ||
{ | ||
if (string.Equals("server", RenderModeString, StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
_renderMode = RenderMode.InteractiveServer; | ||
} | ||
else if (string.Equals("webassembly", RenderModeString, StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
_renderMode = RenderMode.InteractiveWebAssembly; | ||
} | ||
} | ||
} |
27 changes: 27 additions & 0 deletions
27
src/Components/test/testassets/TestContentPackage/MûltibyteÇharacterCompoñent.razor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
<p> | ||
<button class="increment" type="button" @onclick="IncrementCount">Click me</button> | ||
Count: <span class="count">@_count</span> | ||
</p> | ||
|
||
<p> | ||
Is interactive: <span class="is-interactive">@_isInteractive</span> | ||
</p> | ||
|
||
@code { | ||
private int _count = 0; | ||
private bool _isInteractive; | ||
|
||
private void IncrementCount() | ||
{ | ||
_count++; | ||
} | ||
|
||
protected override void OnAfterRender(bool firstRender) | ||
{ | ||
if (firstRender) | ||
{ | ||
_isInteractive = true; | ||
StateHasChanged(); | ||
} | ||
} | ||
} |