diff --git a/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj b/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj index 142f928723f94b1fae5722f1e302cc1e9cf93160..5ac8032197c4684bb1273219a1ee04d0a0c7376c 100644 --- a/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj +++ b/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj @@ -14,7 +14,7 @@ <Compile Include="$(SharedSourceRoot)RazorViews\*.cs" /> <Compile Include="$(SharedSourceRoot)StackTrace\**\*.cs" /> <Compile Include="$(SharedSourceRoot)ErrorPage\**\*.cs" /> - <Compile Include="$(SharedSourceRoot)StaticWebAssets\**\*.cs" /> + <Compile Include="$(SharedSourceRoot)StaticWebAssets\**\*.cs" LinkBase="StaticWebAssets" /> </ItemGroup> <ItemGroup> diff --git a/src/Hosting/Hosting/test/StaticWebAssets/ManifestStaticWebAssetsFileProviderTests.cs b/src/Hosting/Hosting/test/StaticWebAssets/ManifestStaticWebAssetsFileProviderTests.cs index 24f537afc6e6bfd7f3c4a5fe004341e8b022b3ef..160aad77849c1fbe8a4700e53b7ce201a7a9506a 100644 --- a/src/Hosting/Hosting/test/StaticWebAssets/ManifestStaticWebAssetsFileProviderTests.cs +++ b/src/Hosting/Hosting/test/StaticWebAssets/ManifestStaticWebAssetsFileProviderTests.cs @@ -57,6 +57,229 @@ namespace Microsoft.AspNetCore.Hosting.Tests.StaticWebAssets Assert.Equal(expectedResult, file.Exists); } + [Theory] + [InlineData("/img/icon.png", true)] + [InlineData("/Img/hero.gif", true)] + // Note that we've changed the casing of the first segment + [InlineData("/Img/icon.png", false)] + [InlineData("/img/hero.gif", false)] + public void ParseWorksWithNodesThatOnlyDifferOnCasing(string path, bool exists) + { + exists = exists | OperatingSystem.IsWindows(); + // Arrange + using var memoryStream = new MemoryStream(); + using var writer = new StreamWriter(memoryStream); + writer.Write(@"{ + ""ContentRoots"": [ + ""D:\\path\\"", + ""D:\\other\\"" + ], + ""Root"": { + ""Children"": { + ""img"": { + ""Children"": { + ""icon.png"": { + ""Asset"": { + ""ContentRootIndex"": 0, + ""SubPath"": ""icon.png"" + } + } + } + }, + ""Img"": { + ""Children"": { + ""hero.gif"": { + ""Asset"": { + ""ContentRootIndex"": 1, + ""SubPath"": ""hero.gif"" + } + } + } + } + } + } +}"); + var first = new Mock<IFileProvider>(); + first.Setup(s => s.GetFileInfo("icon.png")).Returns(new TestFileInfo() { Name = "icon.png", Exists = true }); + var second = new Mock<IFileProvider>(); + second.Setup(s => s.GetFileInfo("hero.gif")).Returns(new TestFileInfo() { Name = "hero.gif", Exists = true }); + + writer.Flush(); + memoryStream.Seek(0, SeekOrigin.Begin); + var manifest = ManifestStaticWebAssetFileProvider.StaticWebAssetManifest.Parse(memoryStream); + var comparer = ManifestStaticWebAssetFileProvider.StaticWebAssetManifest.PathComparer; + + var provider = new ManifestStaticWebAssetFileProvider( + manifest, + contentRoot => contentRoot switch + { + "D:\\path\\" => first.Object, + "D:\\other\\" => second.Object, + _ => throw new InvalidOperationException("Unknown provider") + }); + + // Act + var file = provider.GetFileInfo(path); + + // Assert + Assert.Equal(exists, file.Exists); + } + + [Theory] + [InlineData("/img/Subdir/icon.png", true)] + [InlineData("/Img/subdir/hero.gif", true)] + // Note that we've changed the casing of the second segment + [InlineData("/img/subdir/icon.png", false)] + [InlineData("/Img/Subdir/hero.gif", false)] + public void ParseWorksWithMergesNodesRecursively(string path, bool exists) + { + // Arrange + exists = exists | OperatingSystem.IsWindows(); + var firstLevelCount = OperatingSystem.IsWindows() ? 1 : 2; + using var memoryStream = new MemoryStream(); + using var writer = new StreamWriter(memoryStream); + writer.Write(@"{ + ""ContentRoots"": [ + ""D:\\path\\"", + ""D:\\other\\"" + ], + ""Root"": { + ""Children"": { + ""img"": { + ""Children"": { + ""Subdir"": { + ""Children"": { + ""icon.png"": { + ""Asset"": { + ""ContentRootIndex"": 0, + ""SubPath"": ""icon.png"" + } + } + } + } + } + }, + ""Img"": { + ""Children"": { + ""subdir"": { + ""Children"": { + ""hero.gif"": { + ""Asset"": { + ""ContentRootIndex"": 1, + ""SubPath"": ""hero.gif"" + } + } + } + } + } + } + } + } +}"); + var first = new Mock<IFileProvider>(); + first.Setup(s => s.GetFileInfo("icon.png")).Returns(new TestFileInfo() { Name = "icon.png", Exists = true }); + var second = new Mock<IFileProvider>(); + second.Setup(s => s.GetFileInfo("hero.gif")).Returns(new TestFileInfo() { Name = "hero.gif", Exists = true }); + + writer.Flush(); + memoryStream.Seek(0, SeekOrigin.Begin); + var manifest = ManifestStaticWebAssetFileProvider.StaticWebAssetManifest.Parse(memoryStream); + var comparer = ManifestStaticWebAssetFileProvider.StaticWebAssetManifest.PathComparer; + + var provider = new ManifestStaticWebAssetFileProvider( + manifest, + contentRoot => contentRoot switch + { + "D:\\path\\" => first.Object, + "D:\\other\\" => second.Object, + _ => throw new InvalidOperationException("Unknown provider") + }); + + // Act + var file = provider.GetFileInfo(path); + + // Assert + Assert.Equal(exists, file.Exists); + Assert.Equal(firstLevelCount, manifest.Root.Children.Count); + Assert.All(manifest.Root.Children.Values, c => Assert.Single(c.Children)); + } + + [Theory] + [InlineData("/img/Subdir", true)] + [InlineData("/Img/subdir/hero.gif", true)] + // Note that we've changed the casing of the second segment + [InlineData("/img/subdir", false)] + [InlineData("/Img/Subdir/hero.gif", false)] + public void ParseWorksFolderAndFileWithDiferentCasing(string path, bool exists) + { + // Arrange + exists = exists | OperatingSystem.IsWindows(); + var firstLevelCount = OperatingSystem.IsWindows() ? 1 : 2; + using var memoryStream = new MemoryStream(); + using var writer = new StreamWriter(memoryStream); + // img/Subdir is a file without extension + writer.Write(@"{ + ""ContentRoots"": [ + ""D:\\path\\"", + ""D:\\other\\"" + ], + ""Root"": { + ""Children"": { + ""img"": { + ""Children"": { + ""Subdir"": { + ""Asset"": { + ""ContentRootIndex"": 0, + ""SubPath"": ""Subdir"" + } + } + } + }, + ""Img"": { + ""Children"": { + ""subdir"": { + ""Children"": { + ""hero.gif"": { + ""Asset"": { + ""ContentRootIndex"": 1, + ""SubPath"": ""hero.gif"" + } + } + } + } + } + } + } + } +}"); + var first = new Mock<IFileProvider>(); + first.Setup(s => s.GetFileInfo("Subdir")).Returns(new TestFileInfo() { Name = "Subdir", Exists = true }); + var second = new Mock<IFileProvider>(); + second.Setup(s => s.GetFileInfo("hero.gif")).Returns(new TestFileInfo() { Name = "hero.gif", Exists = true }); + + writer.Flush(); + memoryStream.Seek(0, SeekOrigin.Begin); + var manifest = ManifestStaticWebAssetFileProvider.StaticWebAssetManifest.Parse(memoryStream); + var comparer = ManifestStaticWebAssetFileProvider.StaticWebAssetManifest.PathComparer; + + var provider = new ManifestStaticWebAssetFileProvider( + manifest, + contentRoot => contentRoot switch + { + "D:\\path\\" => first.Object, + "D:\\other\\" => second.Object, + _ => throw new InvalidOperationException("Unknown provider") + }); + + // Act + var file = provider.GetFileInfo(path); + + // Assert + Assert.Equal(exists, file.Exists); + Assert.Equal(firstLevelCount, manifest.Root.Children.Count); + Assert.All(manifest.Root.Children.Values, c => Assert.Single(c.Children)); + } + [Fact] public void CanFindFileListedOnTheManifest() { diff --git a/src/Shared/StaticWebAssets/StaticWebAssetsFileProvider.cs b/src/Shared/StaticWebAssets/ManifestStaticWebAssetFileProvider.cs similarity index 85% rename from src/Shared/StaticWebAssets/StaticWebAssetsFileProvider.cs rename to src/Shared/StaticWebAssets/ManifestStaticWebAssetFileProvider.cs index 61a5b330d4137a11c439367029e49e110824d665..17b50ab4937df0f7dbb9b396cfda9236855830d1 100644 --- a/src/Shared/StaticWebAssets/StaticWebAssetsFileProvider.cs +++ b/src/Shared/StaticWebAssets/ManifestStaticWebAssetFileProvider.cs @@ -3,7 +3,6 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.FileProviders; @@ -367,9 +366,58 @@ namespace Microsoft.AspNetCore.StaticWebAssets { public override Dictionary<string, StaticWebAssetNode> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - return new Dictionary<string, StaticWebAssetNode>( - JsonSerializer.Deserialize<IDictionary<string, StaticWebAssetNode>>(ref reader, options)!, - StaticWebAssetManifest.PathComparer); + var parsed = JsonSerializer.Deserialize<IDictionary<string, StaticWebAssetNode>>(ref reader, options)!; + var result = new Dictionary<string, StaticWebAssetNode>(StaticWebAssetManifest.PathComparer); + MergeChildren(parsed, result); + return result; + + static void MergeChildren( + IDictionary<string, StaticWebAssetNode> newChildren, + IDictionary<string, StaticWebAssetNode> existing) + { + foreach (var (key, value) in newChildren) + { + if (!existing.TryGetValue(key, out var existingNode)) + { + existing.Add(key, value); + } + else + { + if (value.Patterns != null) + { + if (existingNode.Patterns == null) + { + existingNode.Patterns = value.Patterns; + } + else + { + if (value.Patterns.Length > 0) + { + var newList = new StaticWebAssetPattern[existingNode.Patterns.Length + value.Patterns.Length]; + existingNode.Patterns.CopyTo(newList, 0); + value.Patterns.CopyTo(newList, existingNode.Patterns.Length); + existingNode.Patterns = newList; + } + } + } + + if (value.Children != null) + { + if (existingNode.Children == null) + { + existingNode.Children = value.Children; + } + else + { + if (value.Children.Count > 0) + { + MergeChildren(value.Children, existingNode.Children); + } + } + } + } + } + } } public override void Write(Utf8JsonWriter writer, Dictionary<string, StaticWebAssetNode> value, JsonSerializerOptions options)