| File: EmbeddedFileProvider.cs | Web Access |
| Project: src\aspnetcore\src\FileProviders\Embedded\src\Microsoft.Extensions.FileProviders.Embedded.csproj (Microsoft.Extensions.FileProviders.Embedded) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Text; using Microsoft.AspNetCore.Shared; using Microsoft.Extensions.FileProviders.Embedded; using Microsoft.Extensions.Primitives; namespace Microsoft.Extensions.FileProviders; /// <summary> /// Looks up files using embedded resources in the specified assembly. /// This file provider is case sensitive. /// </summary> public class EmbeddedFileProvider : IFileProvider { private static char[] GetInvalidFileNameChars() => Path.GetInvalidFileNameChars() .Where(c => c != '/' && c != '\\').ToArray(); #if NET8_0_OR_GREATER private static readonly SearchValues<char> _invalidFileNameChars = SearchValues.Create(GetInvalidFileNameChars()); #else private static readonly char[] _invalidFileNameChars = GetInvalidFileNameChars(); #endif private readonly Assembly _assembly; private readonly string _baseNamespace; private readonly DateTimeOffset _lastModified; /// <summary> /// Initializes a new instance of the <see cref="EmbeddedFileProvider" /> class using the specified /// assembly with the base namespace defaulting to the assembly name. /// </summary> /// <param name="assembly">The assembly that contains the embedded resources.</param> public EmbeddedFileProvider(Assembly assembly) : this(assembly, assembly?.GetName()?.Name) { } /// <summary> /// Initializes a new instance of the <see cref="EmbeddedFileProvider" /> class using the specified /// assembly and base namespace. /// </summary> /// <param name="assembly">The assembly that contains the embedded resources.</param> /// <param name="baseNamespace">The base namespace that contains the embedded resources.</param> [UnconditionalSuppressMessage("SingleFile", "IL3000:Assembly.Location", Justification = "The code handles if the Assembly.Location is empty. Workaround https://github.com/dotnet/runtime/issues/83607")] public EmbeddedFileProvider(Assembly assembly, string? baseNamespace) { ArgumentNullThrowHelper.ThrowIfNull(assembly); _baseNamespace = string.IsNullOrEmpty(baseNamespace) ? string.Empty : baseNamespace + "."; _assembly = assembly; _lastModified = DateTimeOffset.UtcNow; var assemblyLocation = assembly.Location; if (!string.IsNullOrEmpty(assemblyLocation)) { try { _lastModified = File.GetLastWriteTimeUtc(assemblyLocation); } catch (PathTooLongException) { } catch (UnauthorizedAccessException) { } } } /// <summary> /// Locates a file at the given path. /// </summary> /// <param name="subpath">The path that identifies the file. </param> /// <returns> /// The file information. Caller must check Exists property. A <see cref="NotFoundFileInfo" /> if the file could /// not be found. /// </returns> public IFileInfo GetFileInfo(string subpath) { if (string.IsNullOrEmpty(subpath)) { return new NotFoundFileInfo(subpath); } var builder = new StringBuilder(_baseNamespace.Length + subpath.Length); builder.Append(_baseNamespace); // Relative paths starting with a leading slash okay if (subpath.StartsWith("/", StringComparison.Ordinal)) { subpath = subpath.Substring(1, subpath.Length - 1); } // 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)) { builder.Append(everettId); builder.Append('.'); } // Append file name of path builder.Append(Path.GetFileName(subpath)); var resourcePath = builder.ToString(); if (HasInvalidPathChars(resourcePath)) { return new NotFoundFileInfo(resourcePath); } var name = Path.GetFileName(subpath); if (_assembly.GetManifestResourceInfo(resourcePath) == null) { return new NotFoundFileInfo(name); } return new EmbeddedResourceFileInfo(_assembly, resourcePath, name, _lastModified); } /// <summary> /// Enumerate a directory at the given path, if any. /// This file provider uses a flat directory structure. Everything under the base namespace is considered to be one /// directory. /// </summary> /// <param name="subpath">The path that identifies the directory</param> /// <returns> /// Contents of the directory. Caller must check Exists property. A <see cref="NotFoundDirectoryContents" /> if no /// resources were found that match <paramref name="subpath" /> /// </returns> public IDirectoryContents GetDirectoryContents(string subpath) { // The file name is assumed to be the remainder of the resource name. if (subpath == null) { return NotFoundDirectoryContents.Singleton; } // EmbeddedFileProvider only supports a flat file structure at the base namespace. if (subpath.Length != 0 && !string.Equals(subpath, "/", StringComparison.Ordinal)) { return NotFoundDirectoryContents.Singleton; } var entries = new List<IFileInfo>(); // TODO: The list of resources in an assembly isn't going to change. Consider caching. var resources = _assembly.GetManifestResourceNames(); for (var i = 0; i < resources.Length; i++) { var resourceName = resources[i]; if (resourceName.StartsWith(_baseNamespace, StringComparison.Ordinal)) { entries.Add(new EmbeddedResourceFileInfo( _assembly, resourceName, resourceName.Substring(_baseNamespace.Length), _lastModified)); } } return new EnumerableDirectoryContents(entries); } /// <summary> /// Embedded files do not change. /// </summary> /// <param name="pattern">This parameter is ignored</param> /// <returns>A <see cref="NullChangeToken" /></returns> public IChangeToken Watch(string pattern) { return NullChangeToken.Singleton; } private static bool HasInvalidPathChars(string path) { 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 }