Browse Source

Wrong replication of resource names in embedded provider (#46067)

Yannici 3 years ago
parent
commit
83d6c56ab6

+ 132 - 10
src/FileProviders/Embedded/src/EmbeddedFileProvider.cs

@@ -3,6 +3,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Reflection;
@@ -87,21 +88,23 @@ public class EmbeddedFileProvider : IFileProvider
         // Relative paths starting with a leading slash okay
         if (subpath.StartsWith("/", StringComparison.Ordinal))
         {
-            builder.Append(subpath, 1, subpath.Length - 1);
-        }
-        else
-        {
-            builder.Append(subpath);
+            subpath = subpath.Substring(1, subpath.Length - 1);
         }
 
-        for (var i = _baseNamespace.Length; i < builder.Length; i++)
+        // Make valid everett id from directory name
+        // The call to this method also replaces directory separator chars to dots
+        var everettId = MakeValidEverettIdentifier(Path.GetDirectoryName(subpath));
+
+        // if directory name was empty, everett id is empty as well
+        if (!string.IsNullOrEmpty(everettId))
         {
-            if (builder[i] == '/' || builder[i] == '\\')
-            {
-                builder[i] = '.';
-            }
+            builder.Append(everettId);
+            builder.Append('.');
         }
 
+        // Append file name of path
+        builder.Append(Path.GetFileName(subpath));
+
         var resourcePath = builder.ToString();
         if (HasInvalidPathChars(resourcePath))
         {
@@ -175,4 +178,123 @@ public class EmbeddedFileProvider : IFileProvider
     {
         return path.IndexOfAny(_invalidFileNameChars) != -1;
     }
+
+    #region Helper methods
+
+    /// <summary>
+    /// Is the character a valid first Everett identifier character?
+    /// </summary>
+    private static bool IsValidEverettIdFirstChar(char c)
+    {
+        return
+            char.IsLetter(c) ||
+            CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.ConnectorPunctuation;
+    }
+
+    /// <summary>
+    /// Is the character a valid Everett identifier character?
+    /// </summary>
+    private static bool IsValidEverettIdChar(char c)
+    {
+        var cat = CharUnicodeInfo.GetUnicodeCategory(c);
+
+        return
+            char.IsLetterOrDigit(c) ||
+            cat == UnicodeCategory.ConnectorPunctuation ||
+            cat == UnicodeCategory.NonSpacingMark ||
+            cat == UnicodeCategory.SpacingCombiningMark ||
+            cat == UnicodeCategory.EnclosingMark;
+    }
+
+    /// <summary>
+    /// Make a folder subname into an Everett-compatible identifier 
+    /// </summary>
+    private static void MakeValidEverettSubFolderIdentifier(StringBuilder builder, string subName)
+    {
+        if (string.IsNullOrEmpty(subName)) { return; }
+
+        // the first character has stronger restrictions than the rest
+        if (IsValidEverettIdFirstChar(subName[0]))
+        {
+            builder.Append(subName[0]);
+        }
+        else
+        {
+            builder.Append('_');
+            if (IsValidEverettIdChar(subName[0]))
+            {
+                // if it is a valid subsequent character, prepend an underscore to it
+                builder.Append(subName[0]);
+            }
+        }
+
+        // process the rest of the subname
+        for (var i = 1; i < subName.Length; i++)
+        {
+            if (!IsValidEverettIdChar(subName[i]))
+            {
+                builder.Append('_');
+            }
+            else
+            {
+                builder.Append(subName[i]);
+            }
+        }
+    }
+
+    /// <summary>
+    /// Make a folder name into an Everett-compatible identifier
+    /// </summary>
+    internal static void MakeValidEverettFolderIdentifier(StringBuilder builder, string name)
+    {
+        if (string.IsNullOrEmpty(name)) { return; }
+
+        // store the original length for use later
+        var length = builder.Length;
+
+        // split folder name into subnames separated by '.', if any
+        var subNames = name.Split('.');
+
+        // convert each subname separately
+        MakeValidEverettSubFolderIdentifier(builder, subNames[0]);
+
+        for (var i = 1; i < subNames.Length; i++)
+        {
+            builder.Append('.');
+            MakeValidEverettSubFolderIdentifier(builder, subNames[i]);
+        }
+
+        // folder name cannot be a single underscore - add another underscore to it
+        if ((builder.Length - length) == 1 && builder[length] == '_')
+        {
+            builder.Append('_');
+        }
+    }
+
+    /// <summary>
+    /// This method is provided for compatibility with Everett which used to convert parts of resource names into
+    /// valid identifiers
+    /// </summary>
+    private static string? MakeValidEverettIdentifier(string? name)
+    {
+        if (string.IsNullOrEmpty(name)) { return name; }
+
+        var everettId = new StringBuilder(name.Length);
+
+        // split the name into folder names
+        var subNames = name.Split(new[] { '/', '\\' });
+
+        // convert every folder name
+        MakeValidEverettFolderIdentifier(everettId, subNames[0]);
+
+        for (var i = 1; i < subNames.Length; i++)
+        {
+            everettId.Append('.');
+            MakeValidEverettFolderIdentifier(everettId, subNames[i]);
+        }
+
+        return everettId.ToString();
+    }
+
+    #endregion
 }

+ 38 - 0
src/FileProviders/Embedded/test/EmbeddedFileProviderTests.cs

@@ -159,6 +159,44 @@ public class EmbeddedFileProviderTests
         Assert.Equal("File.txt", fileInfo.Name);
     }
 
+    public static TheoryData GetFileInfo_LocatesFilesUnderSubDirectories_IfDirectoriesContainsInvalidEverettCharData
+    {
+        get
+        {
+            var theoryData = new TheoryData<string>
+                {
+                    "sub/sub-dir/File3.txt"
+                };
+
+            if (TestPlatformHelper.IsWindows)
+            {
+                theoryData.Add("sub\\sub-dir\\File3.txt");
+            }
+
+            return theoryData;
+        }
+    }
+
+    [Theory]
+    [MemberData(nameof(GetFileInfo_LocatesFilesUnderSubDirectories_IfDirectoriesContainsInvalidEverettCharData))]
+    public void GetFileInfo_LocatesFilesUnderSubDirectories_IfDirectoriesContainsInvalidEverettChar(string path)
+    {
+        // Arrange
+        var provider = new EmbeddedFileProvider(GetType().Assembly);
+
+        // Act
+        var fileInfo = provider.GetFileInfo(path);
+
+        // Assert
+        Assert.NotNull(fileInfo);
+        Assert.True(fileInfo.Exists);
+        Assert.NotEqual(default(DateTimeOffset), fileInfo.LastModified);
+        Assert.True(fileInfo.Length > 0);
+        Assert.False(fileInfo.IsDirectory);
+        Assert.Null(fileInfo.PhysicalPath);
+        Assert.Equal("File3.txt", fileInfo.Name);
+    }
+
     [Theory]
     [InlineData("")]
     [InlineData("/")]

+ 4 - 0
src/FileProviders/Embedded/test/Microsoft.Extensions.FileProviders.Embedded.Tests.csproj

@@ -8,6 +8,10 @@
     <EmbeddedResource Include="File.txt;sub\**\*;Resources\**\*" />
   </ItemGroup>
 
+  <ItemGroup>
+    <None Remove="sub\sub-dir\File3.txt" />
+  </ItemGroup>
+
   <ItemGroup>
     <Reference Include="Microsoft.Extensions.FileProviders.Abstractions" />
     <Reference Include="Microsoft.Extensions.FileProviders.Embedded" />

BIN
src/FileProviders/Embedded/test/sub/sub-dir/File3.txt


+ 12 - 0
src/FileProviders/FileProviders.slnf

@@ -0,0 +1,12 @@
+{
+  "solution": {
+    "path": "..\\..\\AspNetCore.sln",
+    "projects": [
+      "src\\FileProviders\\Embedded\\src\\Microsoft.Extensions.FileProviders.Embedded.csproj",
+      "src\\FileProviders\\Embedded\\test\\Microsoft.Extensions.FileProviders.Embedded.Tests.csproj",
+      "src\\FileProviders\\Manifest.MSBuildTask\\src\\Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.csproj",
+      "src\\FileProviders\\Manifest.MSBuildTask\\test\\Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.Tests.csproj",
+      "src\\Testing\\src\\Microsoft.AspNetCore.Testing.csproj"
+    ]
+  }
+}

+ 3 - 0
src/FileProviders/startvs.cmd

@@ -0,0 +1,3 @@
+@ECHO OFF
+
+%~dp0..\..\startvs.cmd %~dp0FileProviders.slnf