Skip to content

Commit 95d125e

Browse files
Copilotjaviercnlewing
authored
[release/10.0.1xx] URL-encode scoped CSS Link headers for non-ASCII project names (#51039)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: javiercn <[email protected]> Co-authored-by: Javier Calvarro Nelson <[email protected]> Co-authored-by: Larry Ewing <[email protected]>
1 parent d9308af commit 95d125e

File tree

2 files changed

+112
-0
lines changed

2 files changed

+112
-0
lines changed

src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetEndpointsManifest.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ public override bool Execute()
116116
endpoint.AssetFile = asset.ResolvedAsset.ComputeTargetPath("", '/', StaticWebAssetTokenResolver.Instance);
117117
endpoint.Route = route;
118118

119+
EncodeLinkHeadersIfNeeded(endpoint);
120+
119121
Log.LogMessage(MessageImportance.Low, "Including endpoint '{0}' for asset '{1}' with final location '{2}'", endpoint.Route, endpoint.AssetFile, asset.TargetPath);
120122
}
121123

@@ -137,6 +139,48 @@ public override bool Execute()
137139
return !Log.HasLoggedErrors;
138140
}
139141

142+
private static void EncodeLinkHeadersIfNeeded(StaticWebAssetEndpoint endpoint)
143+
{
144+
for (var i = 0; i < endpoint.ResponseHeaders.Length; i++)
145+
{
146+
ref var header = ref endpoint.ResponseHeaders[i];
147+
if (!string.Equals(header.Name, "Link", StringComparison.OrdinalIgnoreCase))
148+
{
149+
continue;
150+
}
151+
var headerValues = header.Value.Split([','], StringSplitOptions.RemoveEmptyEntries);
152+
for (var j = 0; j < headerValues.Length; j++)
153+
{
154+
ref var value = ref headerValues[j];
155+
value = EncodeHeaderValue(value);
156+
}
157+
header.Value = string.Join(",", headerValues);
158+
}
159+
}
160+
161+
private static string EncodeHeaderValue(string header)
162+
{
163+
var index = header.IndexOf('<');
164+
if (index == -1)
165+
{
166+
return header;
167+
}
168+
index++;
169+
var endIndex = header.IndexOf('>', index);
170+
if (endIndex == -1)
171+
{
172+
return header;
173+
}
174+
var link = header.AsSpan(index, endIndex - index).ToString();
175+
var segments = link.Split('/');
176+
for (var j = 0; j < segments.Length; j++)
177+
{
178+
segments[j] = System.Net.WebUtility.UrlEncode(segments[j]);
179+
}
180+
var encoded = string.Join("/", segments);
181+
return $"{header.Substring(0, index)}{encoded}{header.Substring(endIndex)}";
182+
}
183+
140184
private static (string, string[]) ParseAndSortPatterns(string patterns)
141185
{
142186
if (string.IsNullOrEmpty(patterns))

test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ScopedCssIntegrationTests.cs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#nullable disable
55

6+
using System.Text.Json;
67
using System.Text.RegularExpressions;
78
using Microsoft.AspNetCore.StaticWebAssets.Tasks;
89

@@ -617,5 +618,72 @@ public void RegeneratingScopedCss_ForProjectWithReferences()
617618
text.Should().Contain("background-color: orangered");
618619
text.Should().MatchRegex(""".*@import '_content/ClassLibrary/ClassLibrary\.[a-zA-Z0-9]+\.bundle\.scp\.css.*""");
619620
}
621+
622+
[Fact]
623+
public void Build_GeneratesUrlEncodedLinkHeaderForNonAsciiProjectName()
624+
{
625+
var testAsset = "RazorAppWithPackageAndP2PReference";
626+
ProjectDirectory = CreateAspNetSdkTestAsset(testAsset);
627+
628+
// Rename the ClassLibrary project to have non-ASCII characters
629+
var originalLibPath = Path.Combine(ProjectDirectory.Path, "AnotherClassLib");
630+
var newLibPath = Path.Combine(ProjectDirectory.Path, "项目");
631+
Directory.Move(originalLibPath, newLibPath);
632+
633+
// Update the project file to set the assembly name and package ID
634+
var libProjectFile = Path.Combine(newLibPath, "AnotherClassLib.csproj");
635+
var newLibProjectFile = Path.Combine(newLibPath, "项目.csproj");
636+
File.Move(libProjectFile, newLibProjectFile);
637+
638+
// Add assembly name property to ensure consistent naming
639+
var libProjectContent = File.ReadAllText(newLibProjectFile);
640+
// Find the first PropertyGroup closing tag and replace it
641+
var targetPattern = "</PropertyGroup>";
642+
var replacement = " <AssemblyName>项目</AssemblyName>\n <PackageId>项目</PackageId>\n </PropertyGroup>";
643+
var index = libProjectContent.IndexOf(targetPattern);
644+
if (index >= 0)
645+
{
646+
libProjectContent = libProjectContent.Substring(0, index) + replacement + libProjectContent.Substring(index + targetPattern.Length);
647+
}
648+
File.WriteAllText(newLibProjectFile, libProjectContent);
649+
650+
// Update the main project to reference the renamed library
651+
var mainProjectFile = Path.Combine(ProjectDirectory.Path, "AppWithPackageAndP2PReference", "AppWithPackageAndP2PReference.csproj");
652+
var mainProjectContent = File.ReadAllText(mainProjectFile);
653+
mainProjectContent = mainProjectContent.Replace(@"..\AnotherClassLib\AnotherClassLib.csproj", @"..\项目\项目.csproj");
654+
File.WriteAllText(mainProjectFile, mainProjectContent);
655+
656+
// Ensure library has scoped CSS
657+
var libCssFile = Path.Combine(newLibPath, "Views", "Shared", "Index.cshtml.css");
658+
if (!File.Exists(libCssFile))
659+
{
660+
Directory.CreateDirectory(Path.GetDirectoryName(libCssFile));
661+
File.WriteAllText(libCssFile, ".test { color: red; }");
662+
}
663+
664+
EnsureLocalPackagesExists();
665+
666+
var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference");
667+
ExecuteCommand(restore).Should().Pass();
668+
669+
var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference");
670+
ExecuteCommand(build).Should().Pass();
671+
672+
var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString();
673+
674+
// Check that the staticwebassets.build.endpoints.json file contains URL-encoded characters
675+
var endpointsFile = Path.Combine(intermediateOutputPath, "staticwebassets.build.endpoints.json");
676+
new FileInfo(endpointsFile).Should().Exist();
677+
678+
var endpointsContent = File.ReadAllText(endpointsFile);
679+
var json = JsonSerializer.Deserialize<StaticWebAssetEndpointsManifest>(endpointsContent, new JsonSerializerOptions(JsonSerializerDefaults.Web));
680+
681+
var styles = json.Endpoints.Where(e => e.Route.EndsWith("styles.css"));
682+
683+
foreach (var styleEndpoint in styles)
684+
{
685+
styleEndpoint.ResponseHeaders.Should().Contain(h => h.Name.Equals("Link", StringComparison.OrdinalIgnoreCase) && h.Value.Contains("%E9%A1%B9%E7%9B%AE"));
686+
}
687+
}
620688
}
621689
}

0 commit comments

Comments
 (0)