Skip to content

Commit 24040fa

Browse files
authored
[Static web assets] Fix casing issues during parsing (#35564)
* Merges manifest nodes in case-insensitive OSes
1 parent 764ac0a commit 24040fa

File tree

3 files changed

+276
-5
lines changed

3 files changed

+276
-5
lines changed

src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<Compile Include="$(SharedSourceRoot)RazorViews\*.cs" />
1515
<Compile Include="$(SharedSourceRoot)StackTrace\**\*.cs" />
1616
<Compile Include="$(SharedSourceRoot)ErrorPage\**\*.cs" />
17-
<Compile Include="$(SharedSourceRoot)StaticWebAssets\**\*.cs" />
17+
<Compile Include="$(SharedSourceRoot)StaticWebAssets\**\*.cs" LinkBase="StaticWebAssets" />
1818
</ItemGroup>
1919

2020
<ItemGroup>

src/Hosting/Hosting/test/StaticWebAssets/ManifestStaticWebAssetsFileProviderTests.cs

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,229 @@ public void GetFileInfoPrefixRespectsOsCaseSensitivity()
5757
Assert.Equal(expectedResult, file.Exists);
5858
}
5959

60+
[Theory]
61+
[InlineData("/img/icon.png", true)]
62+
[InlineData("/Img/hero.gif", true)]
63+
// Note that we've changed the casing of the first segment
64+
[InlineData("/Img/icon.png", false)]
65+
[InlineData("/img/hero.gif", false)]
66+
public void ParseWorksWithNodesThatOnlyDifferOnCasing(string path, bool exists)
67+
{
68+
exists = exists | OperatingSystem.IsWindows();
69+
// Arrange
70+
using var memoryStream = new MemoryStream();
71+
using var writer = new StreamWriter(memoryStream);
72+
writer.Write(@"{
73+
""ContentRoots"": [
74+
""D:\\path\\"",
75+
""D:\\other\\""
76+
],
77+
""Root"": {
78+
""Children"": {
79+
""img"": {
80+
""Children"": {
81+
""icon.png"": {
82+
""Asset"": {
83+
""ContentRootIndex"": 0,
84+
""SubPath"": ""icon.png""
85+
}
86+
}
87+
}
88+
},
89+
""Img"": {
90+
""Children"": {
91+
""hero.gif"": {
92+
""Asset"": {
93+
""ContentRootIndex"": 1,
94+
""SubPath"": ""hero.gif""
95+
}
96+
}
97+
}
98+
}
99+
}
100+
}
101+
}");
102+
var first = new Mock<IFileProvider>();
103+
first.Setup(s => s.GetFileInfo("icon.png")).Returns(new TestFileInfo() { Name = "icon.png", Exists = true });
104+
var second = new Mock<IFileProvider>();
105+
second.Setup(s => s.GetFileInfo("hero.gif")).Returns(new TestFileInfo() { Name = "hero.gif", Exists = true });
106+
107+
writer.Flush();
108+
memoryStream.Seek(0, SeekOrigin.Begin);
109+
var manifest = ManifestStaticWebAssetFileProvider.StaticWebAssetManifest.Parse(memoryStream);
110+
var comparer = ManifestStaticWebAssetFileProvider.StaticWebAssetManifest.PathComparer;
111+
112+
var provider = new ManifestStaticWebAssetFileProvider(
113+
manifest,
114+
contentRoot => contentRoot switch
115+
{
116+
"D:\\path\\" => first.Object,
117+
"D:\\other\\" => second.Object,
118+
_ => throw new InvalidOperationException("Unknown provider")
119+
});
120+
121+
// Act
122+
var file = provider.GetFileInfo(path);
123+
124+
// Assert
125+
Assert.Equal(exists, file.Exists);
126+
}
127+
128+
[Theory]
129+
[InlineData("/img/Subdir/icon.png", true)]
130+
[InlineData("/Img/subdir/hero.gif", true)]
131+
// Note that we've changed the casing of the second segment
132+
[InlineData("/img/subdir/icon.png", false)]
133+
[InlineData("/Img/Subdir/hero.gif", false)]
134+
public void ParseWorksWithMergesNodesRecursively(string path, bool exists)
135+
{
136+
// Arrange
137+
exists = exists | OperatingSystem.IsWindows();
138+
var firstLevelCount = OperatingSystem.IsWindows() ? 1 : 2;
139+
using var memoryStream = new MemoryStream();
140+
using var writer = new StreamWriter(memoryStream);
141+
writer.Write(@"{
142+
""ContentRoots"": [
143+
""D:\\path\\"",
144+
""D:\\other\\""
145+
],
146+
""Root"": {
147+
""Children"": {
148+
""img"": {
149+
""Children"": {
150+
""Subdir"": {
151+
""Children"": {
152+
""icon.png"": {
153+
""Asset"": {
154+
""ContentRootIndex"": 0,
155+
""SubPath"": ""icon.png""
156+
}
157+
}
158+
}
159+
}
160+
}
161+
},
162+
""Img"": {
163+
""Children"": {
164+
""subdir"": {
165+
""Children"": {
166+
""hero.gif"": {
167+
""Asset"": {
168+
""ContentRootIndex"": 1,
169+
""SubPath"": ""hero.gif""
170+
}
171+
}
172+
}
173+
}
174+
}
175+
}
176+
}
177+
}
178+
}");
179+
var first = new Mock<IFileProvider>();
180+
first.Setup(s => s.GetFileInfo("icon.png")).Returns(new TestFileInfo() { Name = "icon.png", Exists = true });
181+
var second = new Mock<IFileProvider>();
182+
second.Setup(s => s.GetFileInfo("hero.gif")).Returns(new TestFileInfo() { Name = "hero.gif", Exists = true });
183+
184+
writer.Flush();
185+
memoryStream.Seek(0, SeekOrigin.Begin);
186+
var manifest = ManifestStaticWebAssetFileProvider.StaticWebAssetManifest.Parse(memoryStream);
187+
var comparer = ManifestStaticWebAssetFileProvider.StaticWebAssetManifest.PathComparer;
188+
189+
var provider = new ManifestStaticWebAssetFileProvider(
190+
manifest,
191+
contentRoot => contentRoot switch
192+
{
193+
"D:\\path\\" => first.Object,
194+
"D:\\other\\" => second.Object,
195+
_ => throw new InvalidOperationException("Unknown provider")
196+
});
197+
198+
// Act
199+
var file = provider.GetFileInfo(path);
200+
201+
// Assert
202+
Assert.Equal(exists, file.Exists);
203+
Assert.Equal(firstLevelCount, manifest.Root.Children.Count);
204+
Assert.All(manifest.Root.Children.Values, c => Assert.Single(c.Children));
205+
}
206+
207+
[Theory]
208+
[InlineData("/img/Subdir", true)]
209+
[InlineData("/Img/subdir/hero.gif", true)]
210+
// Note that we've changed the casing of the second segment
211+
[InlineData("/img/subdir", false)]
212+
[InlineData("/Img/Subdir/hero.gif", false)]
213+
public void ParseWorksFolderAndFileWithDiferentCasing(string path, bool exists)
214+
{
215+
// Arrange
216+
exists = exists | OperatingSystem.IsWindows();
217+
var firstLevelCount = OperatingSystem.IsWindows() ? 1 : 2;
218+
using var memoryStream = new MemoryStream();
219+
using var writer = new StreamWriter(memoryStream);
220+
// img/Subdir is a file without extension
221+
writer.Write(@"{
222+
""ContentRoots"": [
223+
""D:\\path\\"",
224+
""D:\\other\\""
225+
],
226+
""Root"": {
227+
""Children"": {
228+
""img"": {
229+
""Children"": {
230+
""Subdir"": {
231+
""Asset"": {
232+
""ContentRootIndex"": 0,
233+
""SubPath"": ""Subdir""
234+
}
235+
}
236+
}
237+
},
238+
""Img"": {
239+
""Children"": {
240+
""subdir"": {
241+
""Children"": {
242+
""hero.gif"": {
243+
""Asset"": {
244+
""ContentRootIndex"": 1,
245+
""SubPath"": ""hero.gif""
246+
}
247+
}
248+
}
249+
}
250+
}
251+
}
252+
}
253+
}
254+
}");
255+
var first = new Mock<IFileProvider>();
256+
first.Setup(s => s.GetFileInfo("Subdir")).Returns(new TestFileInfo() { Name = "Subdir", Exists = true });
257+
var second = new Mock<IFileProvider>();
258+
second.Setup(s => s.GetFileInfo("hero.gif")).Returns(new TestFileInfo() { Name = "hero.gif", Exists = true });
259+
260+
writer.Flush();
261+
memoryStream.Seek(0, SeekOrigin.Begin);
262+
var manifest = ManifestStaticWebAssetFileProvider.StaticWebAssetManifest.Parse(memoryStream);
263+
var comparer = ManifestStaticWebAssetFileProvider.StaticWebAssetManifest.PathComparer;
264+
265+
var provider = new ManifestStaticWebAssetFileProvider(
266+
manifest,
267+
contentRoot => contentRoot switch
268+
{
269+
"D:\\path\\" => first.Object,
270+
"D:\\other\\" => second.Object,
271+
_ => throw new InvalidOperationException("Unknown provider")
272+
});
273+
274+
// Act
275+
var file = provider.GetFileInfo(path);
276+
277+
// Assert
278+
Assert.Equal(exists, file.Exists);
279+
Assert.Equal(firstLevelCount, manifest.Root.Children.Count);
280+
Assert.All(manifest.Root.Children.Values, c => Assert.Single(c.Children));
281+
}
282+
60283
[Fact]
61284
public void CanFindFileListedOnTheManifest()
62285
{

src/Shared/StaticWebAssets/StaticWebAssetsFileProvider.cs renamed to src/Shared/StaticWebAssets/ManifestStaticWebAssetFileProvider.cs

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

44
using System.Collections;
55
using System.Diagnostics.CodeAnalysis;
6-
using System.Linq;
76
using System.Text.Json;
87
using System.Text.Json.Serialization;
98
using Microsoft.Extensions.FileProviders;
@@ -367,9 +366,58 @@ private sealed class OSBasedCaseConverter : JsonConverter<Dictionary<string, Sta
367366
{
368367
public override Dictionary<string, StaticWebAssetNode> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
369368
{
370-
return new Dictionary<string, StaticWebAssetNode>(
371-
JsonSerializer.Deserialize<IDictionary<string, StaticWebAssetNode>>(ref reader, options)!,
372-
StaticWebAssetManifest.PathComparer);
369+
var parsed = JsonSerializer.Deserialize<IDictionary<string, StaticWebAssetNode>>(ref reader, options)!;
370+
var result = new Dictionary<string, StaticWebAssetNode>(StaticWebAssetManifest.PathComparer);
371+
MergeChildren(parsed, result);
372+
return result;
373+
374+
static void MergeChildren(
375+
IDictionary<string, StaticWebAssetNode> newChildren,
376+
IDictionary<string, StaticWebAssetNode> existing)
377+
{
378+
foreach (var (key, value) in newChildren)
379+
{
380+
if (!existing.TryGetValue(key, out var existingNode))
381+
{
382+
existing.Add(key, value);
383+
}
384+
else
385+
{
386+
if (value.Patterns != null)
387+
{
388+
if (existingNode.Patterns == null)
389+
{
390+
existingNode.Patterns = value.Patterns;
391+
}
392+
else
393+
{
394+
if (value.Patterns.Length > 0)
395+
{
396+
var newList = new StaticWebAssetPattern[existingNode.Patterns.Length + value.Patterns.Length];
397+
existingNode.Patterns.CopyTo(newList, 0);
398+
value.Patterns.CopyTo(newList, existingNode.Patterns.Length);
399+
existingNode.Patterns = newList;
400+
}
401+
}
402+
}
403+
404+
if (value.Children != null)
405+
{
406+
if (existingNode.Children == null)
407+
{
408+
existingNode.Children = value.Children;
409+
}
410+
else
411+
{
412+
if (value.Children.Count > 0)
413+
{
414+
MergeChildren(value.Children, existingNode.Children);
415+
}
416+
}
417+
}
418+
}
419+
}
420+
}
373421
}
374422

375423
public override void Write(Utf8JsonWriter writer, Dictionary<string, StaticWebAssetNode> value, JsonSerializerOptions options)

0 commit comments

Comments
 (0)