From 574a034ddd13f71c06586615b3a5b57884bb2afc Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Fri, 2 Nov 2018 03:25:45 -0700 Subject: [PATCH 1/2] Reorganize source code in preparation to move into aspnet/Extensions Prior to reorganization, this source code was found in https://github.com/aspnet/FileSystem/tree/dotnet/extensions@baebb8b0c672ab37bac72d7196da1b919d362cc5 \n\nCommit migrated from https://github.com/dotnet/extensions/commit/c087cadf1dfdbd2b8785ef764e5ef58a1a7e5ed0 --- src/FileProviders/Directory.Build.props | 8 + .../Embedded/src/EmbeddedFileProvider.cs | 181 ++++++++ .../Embedded/src/EmbeddedResourceFileInfo.cs | 94 ++++ .../src/EnumerableDirectoryContents.cs | 39 ++ .../src/Manifest/EmbeddedFilesManifest.cs | 91 ++++ .../src/Manifest/ManifestDirectory.cs | 127 ++++++ .../src/Manifest/ManifestDirectoryContents.cs | 72 +++ .../src/Manifest/ManifestDirectoryInfo.cs | 39 ++ .../Embedded/src/Manifest/ManifestEntry.cs | 34 ++ .../Embedded/src/Manifest/ManifestFile.cs | 31 ++ .../Embedded/src/Manifest/ManifestFileInfo.cs | 71 +++ .../Embedded/src/Manifest/ManifestParser.cs | 159 +++++++ .../src/Manifest/ManifestRootDirectory.cs | 16 + .../src/Manifest/ManifestSinkDirectory.cs | 22 + .../src/ManifestEmbeddedFileProvider.cs | 153 +++++++ ...t.Extensions.FileProviders.Embedded.csproj | 46 ++ ...t.Extensions.FileProviders.Embedded.nuspec | 33 ++ .../Embedded/src/Properties/AssemblyInfo.cs | 4 + .../Embedded/src/baseline.netcore.json | 343 ++++++++++++++ ...ft.Extensions.FileProviders.Embedded.props | 17 + ....Extensions.FileProviders.Embedded.targets | 69 +++ ...ft.Extensions.FileProviders.Embedded.props | 3 + ....Extensions.FileProviders.Embedded.targets | 3 + .../test/EmbeddedFileProviderTests.cs | 231 ++++++++++ src/FileProviders/Embedded/test/File.txt | 1 + .../Embedded/test/FileInfoComparer.cs | 34 ++ .../Manifest/EmbeddedFilesManifestTests.cs | 58 +++ .../test/Manifest/ManifestEntryTests.cs | 113 +++++ .../test/Manifest/ManifestParserTests.cs | 116 +++++ .../Embedded/test/Manifest/TestEntry.cs | 41 ++ .../test/ManifestEmbeddedFileProviderTests.cs | 428 ++++++++++++++++++ ...nsions.FileProviders.Embedded.Tests.csproj | 15 + .../Embedded/test/Resources/File.txt | 1 + .../ResourcesInSubdirectory/File3.txt | 1 + .../Embedded/test/TestAssembly.cs | 69 +++ .../Embedded/test/TestFileInfo.cs | 34 ++ src/FileProviders/Embedded/test/sub/File2.txt | 1 + .../Embedded/test/sub/dir/File3.txt | Bin 0 -> 6 bytes .../Manifest.MSBuildTask/src/EmbeddedItem.cs | 21 + .../Manifest.MSBuildTask/src/Entry.cs | 120 +++++ .../src/GenerateEmbeddedResourcesManifest.cs | 104 +++++ .../Manifest.MSBuildTask/src/Manifest.cs | 85 ++++ ...ileProviders.Embedded.Manifest.Task.csproj | 25 + .../GenerateEmbeddedResourcesManifestTest.cs | 388 ++++++++++++++++ ...oviders.Embedded.Manifest.Task.Test.csproj | 11 + .../test/SetExtensions.cs | 20 + 46 files changed, 3572 insertions(+) create mode 100644 src/FileProviders/Directory.Build.props create mode 100644 src/FileProviders/Embedded/src/EmbeddedFileProvider.cs create mode 100644 src/FileProviders/Embedded/src/EmbeddedResourceFileInfo.cs create mode 100644 src/FileProviders/Embedded/src/EnumerableDirectoryContents.cs create mode 100644 src/FileProviders/Embedded/src/Manifest/EmbeddedFilesManifest.cs create mode 100644 src/FileProviders/Embedded/src/Manifest/ManifestDirectory.cs create mode 100644 src/FileProviders/Embedded/src/Manifest/ManifestDirectoryContents.cs create mode 100644 src/FileProviders/Embedded/src/Manifest/ManifestDirectoryInfo.cs create mode 100644 src/FileProviders/Embedded/src/Manifest/ManifestEntry.cs create mode 100644 src/FileProviders/Embedded/src/Manifest/ManifestFile.cs create mode 100644 src/FileProviders/Embedded/src/Manifest/ManifestFileInfo.cs create mode 100644 src/FileProviders/Embedded/src/Manifest/ManifestParser.cs create mode 100644 src/FileProviders/Embedded/src/Manifest/ManifestRootDirectory.cs create mode 100644 src/FileProviders/Embedded/src/Manifest/ManifestSinkDirectory.cs create mode 100644 src/FileProviders/Embedded/src/ManifestEmbeddedFileProvider.cs create mode 100644 src/FileProviders/Embedded/src/Microsoft.Extensions.FileProviders.Embedded.csproj create mode 100644 src/FileProviders/Embedded/src/Microsoft.Extensions.FileProviders.Embedded.nuspec create mode 100644 src/FileProviders/Embedded/src/Properties/AssemblyInfo.cs create mode 100644 src/FileProviders/Embedded/src/baseline.netcore.json create mode 100644 src/FileProviders/Embedded/src/build/netstandard2.0/Microsoft.Extensions.FileProviders.Embedded.props create mode 100644 src/FileProviders/Embedded/src/build/netstandard2.0/Microsoft.Extensions.FileProviders.Embedded.targets create mode 100644 src/FileProviders/Embedded/src/buildMultiTargeting/Microsoft.Extensions.FileProviders.Embedded.props create mode 100644 src/FileProviders/Embedded/src/buildMultiTargeting/Microsoft.Extensions.FileProviders.Embedded.targets create mode 100644 src/FileProviders/Embedded/test/EmbeddedFileProviderTests.cs create mode 100644 src/FileProviders/Embedded/test/File.txt create mode 100644 src/FileProviders/Embedded/test/FileInfoComparer.cs create mode 100644 src/FileProviders/Embedded/test/Manifest/EmbeddedFilesManifestTests.cs create mode 100644 src/FileProviders/Embedded/test/Manifest/ManifestEntryTests.cs create mode 100644 src/FileProviders/Embedded/test/Manifest/ManifestParserTests.cs create mode 100644 src/FileProviders/Embedded/test/Manifest/TestEntry.cs create mode 100644 src/FileProviders/Embedded/test/ManifestEmbeddedFileProviderTests.cs create mode 100644 src/FileProviders/Embedded/test/Microsoft.Extensions.FileProviders.Embedded.Tests.csproj create mode 100644 src/FileProviders/Embedded/test/Resources/File.txt create mode 100644 src/FileProviders/Embedded/test/Resources/ResourcesInSubdirectory/File3.txt create mode 100644 src/FileProviders/Embedded/test/TestAssembly.cs create mode 100644 src/FileProviders/Embedded/test/TestFileInfo.cs create mode 100644 src/FileProviders/Embedded/test/sub/File2.txt create mode 100644 src/FileProviders/Embedded/test/sub/dir/File3.txt create mode 100644 src/FileProviders/Manifest.MSBuildTask/src/EmbeddedItem.cs create mode 100644 src/FileProviders/Manifest.MSBuildTask/src/Entry.cs create mode 100644 src/FileProviders/Manifest.MSBuildTask/src/GenerateEmbeddedResourcesManifest.cs create mode 100644 src/FileProviders/Manifest.MSBuildTask/src/Manifest.cs create mode 100644 src/FileProviders/Manifest.MSBuildTask/src/Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.csproj create mode 100644 src/FileProviders/Manifest.MSBuildTask/test/GenerateEmbeddedResourcesManifestTest.cs create mode 100644 src/FileProviders/Manifest.MSBuildTask/test/Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.Test.csproj create mode 100644 src/FileProviders/Manifest.MSBuildTask/test/SetExtensions.cs diff --git a/src/FileProviders/Directory.Build.props b/src/FileProviders/Directory.Build.props new file mode 100644 index 0000000000..bf4410dcb7 --- /dev/null +++ b/src/FileProviders/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + true + files;filesystem + + diff --git a/src/FileProviders/Embedded/src/EmbeddedFileProvider.cs b/src/FileProviders/Embedded/src/EmbeddedFileProvider.cs new file mode 100644 index 0000000000..75f3f49e49 --- /dev/null +++ b/src/FileProviders/Embedded/src/EmbeddedFileProvider.cs @@ -0,0 +1,181 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using Microsoft.Extensions.FileProviders.Embedded; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Extensions.FileProviders +{ + /// + /// Looks up files using embedded resources in the specified assembly. + /// This file provider is case sensitive. + /// + public class EmbeddedFileProvider : IFileProvider + { + private static readonly char[] _invalidFileNameChars = Path.GetInvalidFileNameChars() + .Where(c => c != '/' && c != '\\').ToArray(); + + private readonly Assembly _assembly; + private readonly string _baseNamespace; + private readonly DateTimeOffset _lastModified; + + /// + /// Initializes a new instance of the class using the specified + /// assembly with the base namespace defaulting to the assembly name. + /// + /// The assembly that contains the embedded resources. + public EmbeddedFileProvider(Assembly assembly) + : this(assembly, assembly?.GetName()?.Name) + { + } + + /// + /// Initializes a new instance of the class using the specified + /// assembly and base namespace. + /// + /// The assembly that contains the embedded resources. + /// The base namespace that contains the embedded resources. + public EmbeddedFileProvider(Assembly assembly, string baseNamespace) + { + if (assembly == null) + { + throw new ArgumentNullException("assembly"); + } + + _baseNamespace = string.IsNullOrEmpty(baseNamespace) ? string.Empty : baseNamespace + "."; + _assembly = assembly; + + _lastModified = DateTimeOffset.UtcNow; + + if (!string.IsNullOrEmpty(_assembly.Location)) + { + try + { + _lastModified = File.GetLastWriteTimeUtc(_assembly.Location); + } + catch (PathTooLongException) + { + } + catch (UnauthorizedAccessException) + { + } + } + } + + /// + /// Locates a file at the given path. + /// + /// The path that identifies the file. + /// + /// The file information. Caller must check Exists property. A if the file could + /// not be found. + /// + 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)) + { + builder.Append(subpath, 1, subpath.Length - 1); + } + else + { + builder.Append(subpath); + } + + for (var i = _baseNamespace.Length; i < builder.Length; i++) + { + if (builder[i] == '/' || builder[i] == '\\') + { + builder[i] = '.'; + } + } + + 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); + } + + /// + /// 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. + /// + /// The path that identifies the directory + /// + /// Contents of the directory. Caller must check Exists property. A if no + /// resources were found that match + /// + 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(); + + // 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); + } + + /// + /// Embedded files do not change. + /// + /// This parameter is ignored + /// A + public IChangeToken Watch(string pattern) + { + return NullChangeToken.Singleton; + } + + private static bool HasInvalidPathChars(string path) + { + return path.IndexOfAny(_invalidFileNameChars) != -1; + } + } +} diff --git a/src/FileProviders/Embedded/src/EmbeddedResourceFileInfo.cs b/src/FileProviders/Embedded/src/EmbeddedResourceFileInfo.cs new file mode 100644 index 0000000000..5dca527342 --- /dev/null +++ b/src/FileProviders/Embedded/src/EmbeddedResourceFileInfo.cs @@ -0,0 +1,94 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Reflection; + +namespace Microsoft.Extensions.FileProviders.Embedded +{ + /// + /// Represents a file embedded in an assembly. + /// + public class EmbeddedResourceFileInfo : IFileInfo + { + private readonly Assembly _assembly; + private readonly string _resourcePath; + + private long? _length; + + /// + /// Initializes a new instance of for an assembly using as the base + /// + /// The assembly that contains the embedded resource + /// The path to the embedded resource + /// An arbitrary name for this instance + /// The to use for + public EmbeddedResourceFileInfo( + Assembly assembly, + string resourcePath, + string name, + DateTimeOffset lastModified) + { + _assembly = assembly; + _resourcePath = resourcePath; + Name = name; + LastModified = lastModified; + } + + /// + /// Always true. + /// + public bool Exists => true; + + /// + /// The length, in bytes, of the embedded resource + /// + public long Length + { + get + { + if (!_length.HasValue) + { + using (var stream = _assembly.GetManifestResourceStream(_resourcePath)) + { + _length = stream.Length; + } + } + return _length.Value; + } + } + + /// + /// Always null. + /// + public string PhysicalPath => null; + + /// + /// The name of embedded file + /// + public string Name { get; } + + /// + /// The time, in UTC, when the was created + /// + public DateTimeOffset LastModified { get; } + + /// + /// Always false. + /// + public bool IsDirectory => false; + + /// + public Stream CreateReadStream() + { + var stream = _assembly.GetManifestResourceStream(_resourcePath); + if (!_length.HasValue) + { + _length = stream.Length; + } + + return stream; + } + } +} diff --git a/src/FileProviders/Embedded/src/EnumerableDirectoryContents.cs b/src/FileProviders/Embedded/src/EnumerableDirectoryContents.cs new file mode 100644 index 0000000000..012723eba6 --- /dev/null +++ b/src/FileProviders/Embedded/src/EnumerableDirectoryContents.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.Extensions.FileProviders.Embedded +{ + internal class EnumerableDirectoryContents : IDirectoryContents + { + private readonly IEnumerable _entries; + + public EnumerableDirectoryContents(IEnumerable entries) + { + if (entries == null) + { + throw new ArgumentNullException(nameof(entries)); + } + + _entries = entries; + } + + public bool Exists + { + get { return true; } + } + + public IEnumerator GetEnumerator() + { + return _entries.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _entries.GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/src/FileProviders/Embedded/src/Manifest/EmbeddedFilesManifest.cs b/src/FileProviders/Embedded/src/Manifest/EmbeddedFilesManifest.cs new file mode 100644 index 0000000000..f017b9b289 --- /dev/null +++ b/src/FileProviders/Embedded/src/Manifest/EmbeddedFilesManifest.cs @@ -0,0 +1,91 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Extensions.FileProviders.Embedded.Manifest +{ + internal class EmbeddedFilesManifest + { + private static readonly char[] _invalidFileNameChars = Path.GetInvalidFileNameChars() + .Where(c => c != Path.DirectorySeparatorChar && c != Path.AltDirectorySeparatorChar).ToArray(); + + private static readonly char[] _separators = new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; + + private readonly ManifestDirectory _rootDirectory; + + internal EmbeddedFilesManifest(ManifestDirectory rootDirectory) + { + if (rootDirectory == null) + { + throw new ArgumentNullException(nameof(rootDirectory)); + } + + _rootDirectory = rootDirectory; + } + + internal ManifestEntry ResolveEntry(string path) + { + if (string.IsNullOrEmpty(path) || HasInvalidPathChars(path)) + { + return null; + } + + // trimmed is a string without leading nor trailing path separators + // so if we find an empty string while iterating over the segments + // we know for sure the path is invalid and we treat it as the above + // case by returning null. + // Examples of invalid paths are: //wwwroot /\wwwroot //wwwroot//jquery.js + var trimmed = RemoveLeadingAndTrailingDirectorySeparators(path); + // Paths consisting only of a single path separator like / or \ are ok. + if (trimmed.Length == 0) + { + return _rootDirectory; + } + + var tokenizer = new StringTokenizer(trimmed, _separators); + ManifestEntry currentEntry = _rootDirectory; + foreach (var segment in tokenizer) + { + if (segment.Equals("")) + { + return null; + } + + currentEntry = currentEntry.Traverse(segment); + } + + return currentEntry; + } + + private static StringSegment RemoveLeadingAndTrailingDirectorySeparators(string path) + { + Debug.Assert(path.Length > 0); + var start = Array.IndexOf(_separators, path[0]) == -1 ? 0 : 1; + if (start == path.Length) + { + return StringSegment.Empty; + } + + var end = Array.IndexOf(_separators, path[path.Length - 1]) == -1 ? path.Length : path.Length - 1; + var trimmed = new StringSegment(path, start, end - start); + return trimmed; + } + + internal EmbeddedFilesManifest Scope(string path) + { + if (ResolveEntry(path) is ManifestDirectory directory && directory != ManifestEntry.UnknownPath) + { + return new EmbeddedFilesManifest(directory.ToRootDirectory()); + } + + throw new InvalidOperationException($"Invalid path: '{path}'"); + } + + private static bool HasInvalidPathChars(string path) => path.IndexOfAny(_invalidFileNameChars) != -1; + } +} diff --git a/src/FileProviders/Embedded/src/Manifest/ManifestDirectory.cs b/src/FileProviders/Embedded/src/Manifest/ManifestDirectory.cs new file mode 100644 index 0000000000..b75653a0fb --- /dev/null +++ b/src/FileProviders/Embedded/src/Manifest/ManifestDirectory.cs @@ -0,0 +1,127 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Extensions.FileProviders.Embedded.Manifest +{ + internal class ManifestDirectory : ManifestEntry + { + protected ManifestDirectory(string name, ManifestEntry[] children) + : base(name) + { + if (children == null) + { + throw new ArgumentNullException(nameof(children)); + } + + Children = children; + } + + public IReadOnlyList Children { get; protected set; } + + public override ManifestEntry Traverse(StringSegment segment) + { + if (segment.Equals(".", StringComparison.Ordinal)) + { + return this; + } + + if (segment.Equals("..", StringComparison.Ordinal)) + { + return Parent; + } + + foreach (var child in Children) + { + if (segment.Equals(child.Name, StringComparison.OrdinalIgnoreCase)) + { + return child; + } + } + + return UnknownPath; + } + + public virtual ManifestDirectory ToRootDirectory() => CreateRootDirectory(CopyChildren()); + + public static ManifestDirectory CreateDirectory(string name, ManifestEntry[] children) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException($"'{nameof(name)}' must not be null, empty or whitespace.", nameof(name)); + } + + if (children == null) + { + throw new ArgumentNullException(nameof(children)); + } + + var result = new ManifestDirectory(name, children); + ValidateChildrenAndSetParent(children, result); + + return result; + } + + public static ManifestRootDirectory CreateRootDirectory(ManifestEntry[] children) + { + if (children == null) + { + throw new ArgumentNullException(nameof(children)); + } + + var result = new ManifestRootDirectory(children); + ValidateChildrenAndSetParent(children, result); + + return result; + } + + internal static void ValidateChildrenAndSetParent(ManifestEntry[] children, ManifestDirectory parent) + { + foreach (var child in children) + { + if (child == UnknownPath) + { + throw new InvalidOperationException($"Invalid entry type '{nameof(ManifestSinkDirectory)}'"); + } + + if (child is ManifestRootDirectory) + { + throw new InvalidOperationException($"Can't add a root folder as a child"); + } + + child.SetParent(parent); + } + } + + private ManifestEntry[] CopyChildren() + { + var list = new List(); + for (int i = 0; i < Children.Count; i++) + { + var child = Children[i]; + switch (child) + { + case ManifestSinkDirectory s: + case ManifestRootDirectory r: + throw new InvalidOperationException("Unexpected manifest node."); + case ManifestDirectory d: + var grandChildren = d.CopyChildren(); + var newDirectory = CreateDirectory(d.Name, grandChildren); + list.Add(newDirectory); + break; + case ManifestFile f: + var file = new ManifestFile(f.Name, f.ResourcePath); + list.Add(file); + break; + default: + throw new InvalidOperationException("Unexpected manifest node."); + } + } + + return list.ToArray(); + } + } +} diff --git a/src/FileProviders/Embedded/src/Manifest/ManifestDirectoryContents.cs b/src/FileProviders/Embedded/src/Manifest/ManifestDirectoryContents.cs new file mode 100644 index 0000000000..38903dd196 --- /dev/null +++ b/src/FileProviders/Embedded/src/Manifest/ManifestDirectoryContents.cs @@ -0,0 +1,72 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Microsoft.Extensions.FileProviders.Embedded.Manifest +{ + internal class ManifestDirectoryContents : IDirectoryContents + { + private readonly DateTimeOffset _lastModified; + private IFileInfo[] _entries; + + public ManifestDirectoryContents(Assembly assembly, ManifestDirectory directory, DateTimeOffset lastModified) + { + if (assembly == null) + { + throw new ArgumentNullException(nameof(assembly)); + } + + if (directory == null) + { + throw new ArgumentNullException(nameof(directory)); + } + + Assembly = assembly; + Directory = directory; + _lastModified = lastModified; + } + + public bool Exists => true; + + public Assembly Assembly { get; } + + public ManifestDirectory Directory { get; } + + public IEnumerator GetEnumerator() + { + return EnsureEntries().GetEnumerator(); + + IReadOnlyList EnsureEntries() => _entries = _entries ?? ResolveEntries().ToArray(); + + IEnumerable ResolveEntries() + { + if (Directory == ManifestEntry.UnknownPath) + { + yield break; + } + + foreach (var entry in Directory.Children) + { + switch (entry) + { + case ManifestFile f: + yield return new ManifestFileInfo(Assembly, f, _lastModified); + break; + case ManifestDirectory d: + yield return new ManifestDirectoryInfo(d, _lastModified); + break; + default: + throw new InvalidOperationException("Unknown entry type"); + } + } + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/FileProviders/Embedded/src/Manifest/ManifestDirectoryInfo.cs b/src/FileProviders/Embedded/src/Manifest/ManifestDirectoryInfo.cs new file mode 100644 index 0000000000..bfe850445d --- /dev/null +++ b/src/FileProviders/Embedded/src/Manifest/ManifestDirectoryInfo.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; + +namespace Microsoft.Extensions.FileProviders.Embedded.Manifest +{ + internal class ManifestDirectoryInfo : IFileInfo + { + public ManifestDirectoryInfo(ManifestDirectory directory, DateTimeOffset lastModified) + { + if (directory == null) + { + throw new ArgumentNullException(nameof(directory)); + } + + Directory = directory; + LastModified = lastModified; + } + + public bool Exists => true; + + public long Length => -1; + + public string PhysicalPath => null; + + public string Name => Directory.Name; + + public DateTimeOffset LastModified { get; } + + public bool IsDirectory => true; + + public ManifestDirectory Directory { get; } + + public Stream CreateReadStream() => + throw new InvalidOperationException("Cannot create a stream for a directory."); + } +} diff --git a/src/FileProviders/Embedded/src/Manifest/ManifestEntry.cs b/src/FileProviders/Embedded/src/Manifest/ManifestEntry.cs new file mode 100644 index 0000000000..5c8ead2741 --- /dev/null +++ b/src/FileProviders/Embedded/src/Manifest/ManifestEntry.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Extensions.FileProviders.Embedded.Manifest +{ + internal abstract class ManifestEntry + { + public ManifestEntry(string name) + { + Name = name; + } + + public ManifestEntry Parent { get; private set; } + + public string Name { get; } + + public static ManifestEntry UnknownPath { get; } = ManifestSinkDirectory.Instance; + + protected internal virtual void SetParent(ManifestDirectory directory) + { + if (Parent != null) + { + throw new InvalidOperationException("Directory already has a parent."); + } + + Parent = directory; + } + + public abstract ManifestEntry Traverse(StringSegment segment); + } +} diff --git a/src/FileProviders/Embedded/src/Manifest/ManifestFile.cs b/src/FileProviders/Embedded/src/Manifest/ManifestFile.cs new file mode 100644 index 0000000000..6dd89d3491 --- /dev/null +++ b/src/FileProviders/Embedded/src/Manifest/ManifestFile.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Extensions.FileProviders.Embedded.Manifest +{ + internal class ManifestFile : ManifestEntry + { + public ManifestFile(string name, string resourcePath) + : base(name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException($"'{nameof(name)}' must not be null, empty or whitespace.", nameof(name)); + } + + if (string.IsNullOrWhiteSpace(resourcePath)) + { + throw new ArgumentException($"'{nameof(resourcePath)}' must not be null, empty or whitespace.", nameof(resourcePath)); + } + + ResourcePath = resourcePath; + } + + public string ResourcePath { get; } + + public override ManifestEntry Traverse(StringSegment segment) => UnknownPath; + } +} diff --git a/src/FileProviders/Embedded/src/Manifest/ManifestFileInfo.cs b/src/FileProviders/Embedded/src/Manifest/ManifestFileInfo.cs new file mode 100644 index 0000000000..2329c16f85 --- /dev/null +++ b/src/FileProviders/Embedded/src/Manifest/ManifestFileInfo.cs @@ -0,0 +1,71 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Reflection; + +namespace Microsoft.Extensions.FileProviders.Embedded.Manifest +{ + internal class ManifestFileInfo : IFileInfo + { + private long? _length; + + public ManifestFileInfo(Assembly assembly, ManifestFile file, DateTimeOffset lastModified) + { + if (assembly == null) + { + throw new ArgumentNullException(nameof(assembly)); + } + + if (file == null) + { + throw new ArgumentNullException(nameof(file)); + } + + Assembly = assembly; + ManifestFile = file; + LastModified = lastModified; + } + + public Assembly Assembly { get; } + + public ManifestFile ManifestFile { get; } + + public bool Exists => true; + + public long Length => EnsureLength(); + + public string PhysicalPath => null; + + public string Name => ManifestFile.Name; + + public DateTimeOffset LastModified { get; } + + public bool IsDirectory => false; + + private long EnsureLength() + { + if (_length == null) + { + using (var stream = Assembly.GetManifestResourceStream(ManifestFile.ResourcePath)) + { + _length = stream.Length; + } + } + + return _length.Value; + } + + public Stream CreateReadStream() + { + var stream = Assembly.GetManifestResourceStream(ManifestFile.ResourcePath); + if (!_length.HasValue) + { + _length = stream.Length; + } + + return stream; + } + } +} diff --git a/src/FileProviders/Embedded/src/Manifest/ManifestParser.cs b/src/FileProviders/Embedded/src/Manifest/ManifestParser.cs new file mode 100644 index 0000000000..a478b747ca --- /dev/null +++ b/src/FileProviders/Embedded/src/Manifest/ManifestParser.cs @@ -0,0 +1,159 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Xml; +using System.Xml.Linq; + +namespace Microsoft.Extensions.FileProviders.Embedded.Manifest +{ + internal static class ManifestParser + { + private static readonly string DefaultManifestName = "Microsoft.Extensions.FileProviders.Embedded.Manifest.xml"; + + public static EmbeddedFilesManifest Parse(Assembly assembly) + { + return Parse(assembly, DefaultManifestName); + } + + public static EmbeddedFilesManifest Parse(Assembly assembly, string name) + { + if (assembly == null) + { + throw new ArgumentNullException(nameof(assembly)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + var stream = assembly.GetManifestResourceStream(name); + if (stream == null) + { + throw new InvalidOperationException($"Could not load the embedded file manifest " + + $"'{name}' for assembly '{assembly.GetName().Name}'."); + } + + var document = XDocument.Load(stream); + + var manifest = EnsureElement(document, "Manifest"); + var manifestVersion = EnsureElement(manifest, "ManifestVersion"); + var version = EnsureText(manifestVersion); + if (!string.Equals("1.0", version, StringComparison.Ordinal)) + { + throw new InvalidOperationException($"The embedded file manifest '{name}' for " + + $"assembly '{assembly.GetName().Name}' specifies an unsupported file format" + + $" version: '{version}'."); + } + var fileSystem = EnsureElement(manifest, "FileSystem"); + + var entries = fileSystem.Elements(); + var entriesList = new List(); + foreach (var element in entries) + { + var entry = BuildEntry(element); + entriesList.Add(entry); + } + + ValidateEntries(entriesList); + + var rootDirectory = ManifestDirectory.CreateRootDirectory(entriesList.ToArray()); + + return new EmbeddedFilesManifest(rootDirectory); + + } + + private static void ValidateEntries(List entriesList) + { + for (int i = 0; i < entriesList.Count - 1; i++) + { + for (int j = i + 1; j < entriesList.Count; j++) + { + if (string.Equals(entriesList[i].Name, entriesList[j].Name, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException( + "Found two entries with the same name but different casing:" + + $" '{entriesList[i].Name}' and '{entriesList[j]}'"); + } + } + } + } + + private static ManifestEntry BuildEntry(XElement element) + { + RuntimeHelpers.EnsureSufficientExecutionStack(); + if (element.NodeType != XmlNodeType.Element) + { + throw new InvalidOperationException($"Invalid manifest format. Expected a 'File' or a 'Directory' node:" + + $" '{element.ToString()}'"); + } + + if (string.Equals(element.Name.LocalName, "File", StringComparison.Ordinal)) + { + var entryName = EnsureName(element); + var path = EnsureElement(element, "ResourcePath"); + var pathValue = EnsureText(path); + return new ManifestFile(entryName, pathValue); + } + + if (string.Equals(element.Name.LocalName, "Directory", StringComparison.Ordinal)) + { + var directoryName = EnsureName(element); + var children = new List(); + foreach (var child in element.Elements()) + { + children.Add(BuildEntry(child)); + } + + ValidateEntries(children); + + return ManifestDirectory.CreateDirectory(directoryName, children.ToArray()); + } + + throw new InvalidOperationException($"Invalid manifest format.Expected a 'File' or a 'Directory' node. " + + $"Got '{element.Name.LocalName}' instead."); + } + + private static XElement EnsureElement(XContainer container, string elementName) + { + var element = container.Element(elementName); + if (element == null) + { + throw new InvalidOperationException($"Invalid manifest format. Missing '{elementName}' element name"); + } + + return element; + } + + private static string EnsureName(XElement element) + { + var value = element.Attribute("Name")?.Value; + if (value == null) + { + throw new InvalidOperationException($"Invalid manifest format. '{element.Name}' must contain a 'Name' attribute."); + } + + return value; + } + + private static string EnsureText(XElement element) + { + if (element.Elements().Count() == 0 && + !element.IsEmpty && + element.Nodes().Count() == 1 && + element.FirstNode.NodeType == XmlNodeType.Text) + { + return element.Value; + } + + throw new InvalidOperationException( + $"Invalid manifest format. '{element.Name.LocalName}' must contain " + + $"a text value. '{element.Value}'"); + } + } +} diff --git a/src/FileProviders/Embedded/src/Manifest/ManifestRootDirectory.cs b/src/FileProviders/Embedded/src/Manifest/ManifestRootDirectory.cs new file mode 100644 index 0000000000..1e5999e906 --- /dev/null +++ b/src/FileProviders/Embedded/src/Manifest/ManifestRootDirectory.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.Extensions.FileProviders.Embedded.Manifest +{ + internal class ManifestRootDirectory : ManifestDirectory + { + public ManifestRootDirectory(ManifestEntry[] children) + : base(name: null, children: children) + { + SetParent(ManifestSinkDirectory.Instance); + } + + public override ManifestDirectory ToRootDirectory() => this; + } +} diff --git a/src/FileProviders/Embedded/src/Manifest/ManifestSinkDirectory.cs b/src/FileProviders/Embedded/src/Manifest/ManifestSinkDirectory.cs new file mode 100644 index 0000000000..f14908534f --- /dev/null +++ b/src/FileProviders/Embedded/src/Manifest/ManifestSinkDirectory.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Extensions.FileProviders.Embedded.Manifest +{ + internal class ManifestSinkDirectory : ManifestDirectory + { + private ManifestSinkDirectory() + : base(name: null, children: Array.Empty()) + { + SetParent(this); + Children = new[] { this }; + } + + public static ManifestDirectory Instance { get; } = new ManifestSinkDirectory(); + + public override ManifestEntry Traverse(StringSegment segment) => this; + } +} diff --git a/src/FileProviders/Embedded/src/ManifestEmbeddedFileProvider.cs b/src/FileProviders/Embedded/src/ManifestEmbeddedFileProvider.cs new file mode 100644 index 0000000000..f639a2a812 --- /dev/null +++ b/src/FileProviders/Embedded/src/ManifestEmbeddedFileProvider.cs @@ -0,0 +1,153 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Reflection; +using Microsoft.Extensions.FileProviders.Embedded.Manifest; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Extensions.FileProviders +{ + /// + /// An embedded file provider that uses a manifest compiled in the assembly to + /// reconstruct the original paths of the embedded files when they were embedded + /// into the assembly. + /// + public class ManifestEmbeddedFileProvider : IFileProvider + { + private readonly DateTimeOffset _lastModified; + + /// + /// Initializes a new instance of . + /// + /// The assembly containing the embedded files. + public ManifestEmbeddedFileProvider(Assembly assembly) + : this(assembly, ManifestParser.Parse(assembly), ResolveLastModified(assembly)) { } + + /// + /// Initializes a new instance of . + /// + /// The assembly containing the embedded files. + /// The relative path from the root of the manifest to use as root for the provider. + public ManifestEmbeddedFileProvider(Assembly assembly, string root) + : this(assembly, root, ResolveLastModified(assembly)) + { + } + + /// + /// Initializes a new instance of . + /// + /// The assembly containing the embedded files. + /// The relative path from the root of the manifest to use as root for the provider. + /// The LastModified date to use on the instances + /// returned by this . + public ManifestEmbeddedFileProvider(Assembly assembly, string root, DateTimeOffset lastModified) + : this(assembly, ManifestParser.Parse(assembly).Scope(root), lastModified) + { + } + + /// + /// Initializes a new instance of . + /// + /// The assembly containing the embedded files. + /// The relative path from the root of the manifest to use as root for the provider. + /// The name of the embedded resource containing the manifest. + /// The LastModified date to use on the instances + /// returned by this . + public ManifestEmbeddedFileProvider(Assembly assembly, string root, string manifestName, DateTimeOffset lastModified) + : this(assembly, ManifestParser.Parse(assembly, manifestName).Scope(root), lastModified) + { + } + + internal ManifestEmbeddedFileProvider(Assembly assembly, EmbeddedFilesManifest manifest, DateTimeOffset lastModified) + { + if (assembly == null) + { + throw new ArgumentNullException(nameof(assembly)); + } + + if (manifest == null) + { + throw new ArgumentNullException(nameof(manifest)); + } + + Assembly = assembly; + Manifest = manifest; + _lastModified = lastModified; + } + + /// + /// Gets the for this provider. + /// + public Assembly Assembly { get; } + + internal EmbeddedFilesManifest Manifest { get; } + + /// + public IDirectoryContents GetDirectoryContents(string subpath) + { + var entry = Manifest.ResolveEntry(subpath); + if (entry == null || entry == ManifestEntry.UnknownPath) + { + return NotFoundDirectoryContents.Singleton; + } + + if (!(entry is ManifestDirectory directory)) + { + return NotFoundDirectoryContents.Singleton; + } + + return new ManifestDirectoryContents(Assembly, directory, _lastModified); + } + + /// + public IFileInfo GetFileInfo(string subpath) + { + var entry = Manifest.ResolveEntry(subpath); + switch (entry) + { + case null: + return new NotFoundFileInfo(subpath); + case ManifestFile f: + return new ManifestFileInfo(Assembly, f, _lastModified); + case ManifestDirectory d when d != ManifestEntry.UnknownPath: + return new NotFoundFileInfo(d.Name); + } + + return new NotFoundFileInfo(subpath); + } + + /// + public IChangeToken Watch(string filter) + { + if (filter == null) + { + throw new ArgumentNullException(nameof(filter)); + } + + return NullChangeToken.Singleton; + } + + private static DateTimeOffset ResolveLastModified(Assembly assembly) + { + var result = DateTimeOffset.UtcNow; + + if (!string.IsNullOrEmpty(assembly.Location)) + { + try + { + result = File.GetLastWriteTimeUtc(assembly.Location); + } + catch (PathTooLongException) + { + } + catch (UnauthorizedAccessException) + { + } + } + + return result; + } + } +} diff --git a/src/FileProviders/Embedded/src/Microsoft.Extensions.FileProviders.Embedded.csproj b/src/FileProviders/Embedded/src/Microsoft.Extensions.FileProviders.Embedded.csproj new file mode 100644 index 0000000000..d7ca20b469 --- /dev/null +++ b/src/FileProviders/Embedded/src/Microsoft.Extensions.FileProviders.Embedded.csproj @@ -0,0 +1,46 @@ + + + + Microsoft.Extensions.FileProviders + File provider for files in embedded resources for Microsoft.Extensions.FileProviders. + netstandard2.0 + $(MSBuildProjectName).nuspec + + + + + + + + + + + + id=$(PackageId); + version=$(PackageVersion); + authors=$(Authors); + description=$(Description); + tags=$(PackageTags.Replace(';', ' ')); + licenseUrl=$(PackageLicenseUrl); + projectUrl=$(PackageProjectUrl); + iconUrl=$(PackageIconUrl); + repositoryUrl=$(RepositoryUrl); + repositoryCommit=$(RepositoryCommit); + copyright=$(Copyright); + targetframework=$(TargetFramework); + AssemblyName=$(AssemblyName); + + OutputBinary=@(BuiltProjectOutputGroupOutput); + OutputSymbol=@(DebugSymbolsProjectOutputGroupOutput); + OutputDocumentation=@(DocumentationProjectOutputGroupOutput); + + + TaskAssemblyNetStandard=..\..\Manifest.MSBuildTask\src\bin\$(Configuration)\netstandard1.5\$(AssemblyName).Manifest.Task.dll; + TaskSymbolNetStandard=..\..\Manifest.MSBuildTask\src\bin\$(Configuration)\netstandard1.5\$(AssemblyName).Manifest.Task.pdb; + TaskAssemblyNet461=..\..\Manifest.MSBuildTask\src\bin\$(Configuration)\net461\$(AssemblyName).Manifest.Task.dll; + TaskSymbolNet461=..\..\Manifest.MSBuildTask\src\bin\$(Configuration)\net461\$(AssemblyName).Manifest.Task.pdb; + + + + + diff --git a/src/FileProviders/Embedded/src/Microsoft.Extensions.FileProviders.Embedded.nuspec b/src/FileProviders/Embedded/src/Microsoft.Extensions.FileProviders.Embedded.nuspec new file mode 100644 index 0000000000..0cc5ed823a --- /dev/null +++ b/src/FileProviders/Embedded/src/Microsoft.Extensions.FileProviders.Embedded.nuspec @@ -0,0 +1,33 @@ + + + + $id$ + $version$ + $authors$ + true + $licenseUrl$ + $projectUrl$ + $iconUrl$ + $description$ + $copyright$ + $tags$ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/FileProviders/Embedded/src/Properties/AssemblyInfo.cs b/src/FileProviders/Embedded/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..610a7fa706 --- /dev/null +++ b/src/FileProviders/Embedded/src/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.Extensions.FileProviders.Embedded.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] \ No newline at end of file diff --git a/src/FileProviders/Embedded/src/baseline.netcore.json b/src/FileProviders/Embedded/src/baseline.netcore.json new file mode 100644 index 0000000000..821969ea0b --- /dev/null +++ b/src/FileProviders/Embedded/src/baseline.netcore.json @@ -0,0 +1,343 @@ +{ + "AssemblyIdentity": "Microsoft.Extensions.FileProviders.Embedded, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Extensions.FileProviders.EmbeddedFileProvider", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.Extensions.FileProviders.IFileProvider" + ], + "Members": [ + { + "Kind": "Method", + "Name": "GetFileInfo", + "Parameters": [ + { + "Name": "subpath", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.Extensions.FileProviders.IFileInfo", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.FileProviders.IFileProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetDirectoryContents", + "Parameters": [ + { + "Name": "subpath", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.Extensions.FileProviders.IDirectoryContents", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.FileProviders.IFileProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Watch", + "Parameters": [ + { + "Name": "pattern", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.Extensions.Primitives.IChangeToken", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.FileProviders.IFileProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "assembly", + "Type": "System.Reflection.Assembly" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "assembly", + "Type": "System.Reflection.Assembly" + }, + { + "Name": "baseNamespace", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Extensions.FileProviders.ManifestEmbeddedFileProvider", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.Extensions.FileProviders.IFileProvider" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Assembly", + "Parameters": [], + "ReturnType": "System.Reflection.Assembly", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetDirectoryContents", + "Parameters": [ + { + "Name": "subpath", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.Extensions.FileProviders.IDirectoryContents", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.FileProviders.IFileProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetFileInfo", + "Parameters": [ + { + "Name": "subpath", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.Extensions.FileProviders.IFileInfo", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.FileProviders.IFileProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Watch", + "Parameters": [ + { + "Name": "filter", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.Extensions.Primitives.IChangeToken", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.FileProviders.IFileProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "assembly", + "Type": "System.Reflection.Assembly" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "assembly", + "Type": "System.Reflection.Assembly" + }, + { + "Name": "root", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "assembly", + "Type": "System.Reflection.Assembly" + }, + { + "Name": "root", + "Type": "System.String" + }, + { + "Name": "lastModified", + "Type": "System.DateTimeOffset" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "assembly", + "Type": "System.Reflection.Assembly" + }, + { + "Name": "root", + "Type": "System.String" + }, + { + "Name": "manifestName", + "Type": "System.String" + }, + { + "Name": "lastModified", + "Type": "System.DateTimeOffset" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Extensions.FileProviders.Embedded.EmbeddedResourceFileInfo", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.Extensions.FileProviders.IFileInfo" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Exists", + "Parameters": [], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.FileProviders.IFileInfo", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Length", + "Parameters": [], + "ReturnType": "System.Int64", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.FileProviders.IFileInfo", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_PhysicalPath", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.FileProviders.IFileInfo", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.FileProviders.IFileInfo", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_LastModified", + "Parameters": [], + "ReturnType": "System.DateTimeOffset", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.FileProviders.IFileInfo", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsDirectory", + "Parameters": [], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.FileProviders.IFileInfo", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateReadStream", + "Parameters": [], + "ReturnType": "System.IO.Stream", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.FileProviders.IFileInfo", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "assembly", + "Type": "System.Reflection.Assembly" + }, + { + "Name": "resourcePath", + "Type": "System.String" + }, + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "lastModified", + "Type": "System.DateTimeOffset" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/FileProviders/Embedded/src/build/netstandard2.0/Microsoft.Extensions.FileProviders.Embedded.props b/src/FileProviders/Embedded/src/build/netstandard2.0/Microsoft.Extensions.FileProviders.Embedded.props new file mode 100644 index 0000000000..e913e17321 --- /dev/null +++ b/src/FileProviders/Embedded/src/build/netstandard2.0/Microsoft.Extensions.FileProviders.Embedded.props @@ -0,0 +1,17 @@ + + + false + Microsoft.Extensions.FileProviders.Embedded.Manifest.xml + + + + <_FileProviderTaskFolder Condition="'$(MSBuildRuntimeType)' == 'Core'">netstandard1.5 + <_FileProviderTaskFolder Condition="'$(MSBuildRuntimeType)' != 'Core'">net461 + <_FileProviderTaskAssembly>$(MSBuildThisFileDirectory)..\..\tasks\$(_FileProviderTaskFolder)\Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.dll + + + + + diff --git a/src/FileProviders/Embedded/src/build/netstandard2.0/Microsoft.Extensions.FileProviders.Embedded.targets b/src/FileProviders/Embedded/src/build/netstandard2.0/Microsoft.Extensions.FileProviders.Embedded.targets new file mode 100644 index 0000000000..83505d7fea --- /dev/null +++ b/src/FileProviders/Embedded/src/build/netstandard2.0/Microsoft.Extensions.FileProviders.Embedded.targets @@ -0,0 +1,69 @@ + + + _CalculateEmbeddedFilesManifestInputs;$(PrepareResourceNamesDependsOn) + + + + + + <_GeneratedManifestFile>$(IntermediateOutputPath)$(EmbeddedFilesManifestFileName) + + + + <_FilesForManifest Include="@(EmbeddedResource)" /> + <_FilesForManifest Remove="@(EmbeddedResource->WithMetadataValue('ExcludeFromManifest','true'))" /> + + + + + + + + + + + + <_GeneratedManifestInfoInputsCacheFile>$(IntermediateOutputPath)$(MSBuildProjectName).EmbeddedFilesManifest.cache + + + + + + + + + + + + + + + + + + <_FilesForManifest Remove="@(_FilesForManifest)" /> + <_FilesForManifest Include="@(EmbeddedResource)" /> + <_FilesForManifest Remove="@(EmbeddedResource->WithMetadataValue('ExcludeFromManifest','true'))" /> + + + + + diff --git a/src/FileProviders/Embedded/src/buildMultiTargeting/Microsoft.Extensions.FileProviders.Embedded.props b/src/FileProviders/Embedded/src/buildMultiTargeting/Microsoft.Extensions.FileProviders.Embedded.props new file mode 100644 index 0000000000..87296f28f3 --- /dev/null +++ b/src/FileProviders/Embedded/src/buildMultiTargeting/Microsoft.Extensions.FileProviders.Embedded.props @@ -0,0 +1,3 @@ + + + diff --git a/src/FileProviders/Embedded/src/buildMultiTargeting/Microsoft.Extensions.FileProviders.Embedded.targets b/src/FileProviders/Embedded/src/buildMultiTargeting/Microsoft.Extensions.FileProviders.Embedded.targets new file mode 100644 index 0000000000..9191097036 --- /dev/null +++ b/src/FileProviders/Embedded/src/buildMultiTargeting/Microsoft.Extensions.FileProviders.Embedded.targets @@ -0,0 +1,3 @@ + + + diff --git a/src/FileProviders/Embedded/test/EmbeddedFileProviderTests.cs b/src/FileProviders/Embedded/test/EmbeddedFileProviderTests.cs new file mode 100644 index 0000000000..cb9598a1b4 --- /dev/null +++ b/src/FileProviders/Embedded/test/EmbeddedFileProviderTests.cs @@ -0,0 +1,231 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.Extensions.FileProviders.Embedded.Tests +{ + public class EmbeddedFileProviderTests + { + private static readonly string Namespace = typeof(EmbeddedFileProviderTests).Namespace; + + [Fact] + public void ConstructorWithNullAssemblyThrowsArgumentException() + { + Assert.Throws(() => new EmbeddedFileProvider(null)); + } + + [Fact] + public void GetFileInfo_ReturnsNotFoundFileInfo_IfFileDoesNotExist() + { + // Arrange + var provider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly); + + // Act + var fileInfo = provider.GetFileInfo("DoesNotExist.Txt"); + + // Assert + Assert.NotNull(fileInfo); + Assert.False(fileInfo.Exists); + } + + [Theory] + [InlineData("File.txt")] + [InlineData("/File.txt")] + public void GetFileInfo_ReturnsFilesAtRoot(string filePath) + { + // Arrange + var provider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly); + var expectedFileLength = 8; + + // Act + var fileInfo = provider.GetFileInfo(filePath); + + // Assert + Assert.NotNull(fileInfo); + Assert.True(fileInfo.Exists); + Assert.NotEqual(default(DateTimeOffset), fileInfo.LastModified); + Assert.Equal(expectedFileLength, fileInfo.Length); + Assert.False(fileInfo.IsDirectory); + Assert.Null(fileInfo.PhysicalPath); + Assert.Equal("File.txt", fileInfo.Name); + } + + [Fact] + public void GetFileInfo_ReturnsNotFoundFileInfo_IfFileDoesNotExistUnderSpecifiedNamespace() + { + // Arrange + var provider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly, Namespace + ".SubNamespace"); + + // Act + var fileInfo = provider.GetFileInfo("File.txt"); + + // Assert + Assert.NotNull(fileInfo); + Assert.False(fileInfo.Exists); + } + + [Fact] + public void GetFileInfo_ReturnsNotFoundIfPathStartsWithBackSlash() + { + // Arrange + var provider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly); + + // Act + var fileInfo = provider.GetFileInfo("\\File.txt"); + + // Assert + Assert.NotNull(fileInfo); + Assert.False(fileInfo.Exists); + } + + public static TheoryData GetFileInfo_LocatesFilesUnderSpecifiedNamespaceData + { + get + { + var theoryData = new TheoryData + { + "ResourcesInSubdirectory/File3.txt" + }; + + if (TestPlatformHelper.IsWindows) + { + theoryData.Add("ResourcesInSubdirectory\\File3.txt"); + } + + return theoryData; + } + } + + [Theory] + [MemberData(nameof(GetFileInfo_LocatesFilesUnderSpecifiedNamespaceData))] + public void GetFileInfo_LocatesFilesUnderSpecifiedNamespace(string path) + { + // Arrange + var provider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly, Namespace + ".Resources"); + + // 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); + } + + public static TheoryData GetFileInfo_LocatesFilesUnderSubDirectoriesData + { + get + { + var theoryData = new TheoryData + { + "Resources/File.txt" + }; + + if (TestPlatformHelper.IsWindows) + { + theoryData.Add("Resources\\File.txt"); + } + + return theoryData; + } + } + + [Theory] + [MemberData(nameof(GetFileInfo_LocatesFilesUnderSubDirectoriesData))] + public void GetFileInfo_LocatesFilesUnderSubDirectories(string path) + { + // Arrange + var provider = new EmbeddedFileProvider(GetType().GetTypeInfo().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("File.txt", fileInfo.Name); + } + + [Theory] + [InlineData("")] + [InlineData("/")] + public void GetDirectoryContents_ReturnsAllFilesInFileSystem(string path) + { + // Arrange + var provider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly, Namespace + ".Resources"); + + // Act + var files = provider.GetDirectoryContents(path); + + // Assert + Assert.Collection(files.OrderBy(f => f.Name, StringComparer.Ordinal), + file => Assert.Equal("File.txt", file.Name), + file => Assert.Equal("ResourcesInSubdirectory.File3.txt", file.Name)); + + Assert.False(provider.GetDirectoryContents("file").Exists); + Assert.False(provider.GetDirectoryContents("file/").Exists); + Assert.False(provider.GetDirectoryContents("file.txt").Exists); + Assert.False(provider.GetDirectoryContents("file/txt").Exists); + } + + [Fact] + public void GetDirectoryContents_ReturnsEmptySequence_IfResourcesDoNotExistUnderNamespace() + { + // Arrange + var provider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly, "Unknown.Namespace"); + + // Act + var files = provider.GetDirectoryContents(string.Empty); + + // Assert + Assert.NotNull(files); + Assert.True(files.Exists); + Assert.Empty(files); + } + + [Theory] + [InlineData("Resources")] + [InlineData("/Resources")] + public void GetDirectoryContents_ReturnsNotFoundDirectoryContents_IfHierarchicalPathIsSpecified(string path) + { + // Arrange + var provider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly); + + // Act + var files = provider.GetDirectoryContents(path); + + // Assert + Assert.NotNull(files); + Assert.False(files.Exists); + Assert.Empty(files); + } + + [Fact] + public void Watch_ReturnsNoOpTrigger() + { + // Arange + var provider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly); + + // Act + var token = provider.Watch("Resources/File.txt"); + + // Assert + Assert.NotNull(token); + Assert.False(token.ActiveChangeCallbacks); + Assert.False(token.HasChanged); + } + } +} \ No newline at end of file diff --git a/src/FileProviders/Embedded/test/File.txt b/src/FileProviders/Embedded/test/File.txt new file mode 100644 index 0000000000..357323fbfa --- /dev/null +++ b/src/FileProviders/Embedded/test/File.txt @@ -0,0 +1 @@ +Hello \ No newline at end of file diff --git a/src/FileProviders/Embedded/test/FileInfoComparer.cs b/src/FileProviders/Embedded/test/FileInfoComparer.cs new file mode 100644 index 0000000000..1b4b69b4c1 --- /dev/null +++ b/src/FileProviders/Embedded/test/FileInfoComparer.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Extensions.FileProviders +{ + internal class FileInfoComparer : IEqualityComparer + { + public static FileInfoComparer Instance { get; set; } = new FileInfoComparer(); + + public bool Equals(IFileInfo x, IFileInfo y) + { + if (x == null && y == null) + { + return true; + } + + if ((x == null && y != null) || (x != null && y == null)) + { + return false; + } + + return x.Exists == y.Exists && + x.IsDirectory == y.IsDirectory && + x.Length == y.Length && + string.Equals(x.Name, y.Name, StringComparison.Ordinal) && + string.Equals(x.PhysicalPath, y.PhysicalPath, StringComparison.Ordinal); + } + + public int GetHashCode(IFileInfo obj) => 0; + } +} diff --git a/src/FileProviders/Embedded/test/Manifest/EmbeddedFilesManifestTests.cs b/src/FileProviders/Embedded/test/Manifest/EmbeddedFilesManifestTests.cs new file mode 100644 index 0000000000..107491d34a --- /dev/null +++ b/src/FileProviders/Embedded/test/Manifest/EmbeddedFilesManifestTests.cs @@ -0,0 +1,58 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit; + +namespace Microsoft.Extensions.FileProviders.Embedded.Manifest +{ + public class EmbeddedFilesManifestTests + { + [Theory] + [InlineData("/wwwroot//jquery.validate.js")] + [InlineData("//wwwroot/jquery.validate.js")] + public void ResolveEntry_IgnoresInvalidPaths(string path) + { + // Arrange + var manifest = new EmbeddedFilesManifest( + ManifestDirectory.CreateRootDirectory( + new[] + { + ManifestDirectory.CreateDirectory("wwwroot", + new[] + { + new ManifestFile("jquery.validate.js","wwwroot.jquery.validate.js") + }) + })); + // Act + var entry = manifest.ResolveEntry(path); + + // Assert + Assert.Null(entry); + } + + [Theory] + [InlineData("/")] + [InlineData("./")] + [InlineData("/wwwroot/jquery.validate.js")] + [InlineData("/wwwroot/")] + public void ResolveEntry_AllowsSingleDirectorySeparator(string path) + { + // Arrange + var manifest = new EmbeddedFilesManifest( + ManifestDirectory.CreateRootDirectory( + new[] + { + ManifestDirectory.CreateDirectory("wwwroot", + new[] + { + new ManifestFile("jquery.validate.js","wwwroot.jquery.validate.js") + }) + })); + // Act + var entry = manifest.ResolveEntry(path); + + // Assert + Assert.NotNull(entry); + } + } +} diff --git a/src/FileProviders/Embedded/test/Manifest/ManifestEntryTests.cs b/src/FileProviders/Embedded/test/Manifest/ManifestEntryTests.cs new file mode 100644 index 0000000000..dc1a7e1cdd --- /dev/null +++ b/src/FileProviders/Embedded/test/Manifest/ManifestEntryTests.cs @@ -0,0 +1,113 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Xunit; + +namespace Microsoft.Extensions.FileProviders.Embedded.Manifest +{ + public class ManifestEntryTests + { + [Fact] + public void TraversingAFile_ReturnsUnknownPath() + { + // Arrange + var file = new ManifestFile("a", "a.b.c"); + + // Act + var result = file.Traverse("."); + + // Assert + Assert.Equal(ManifestEntry.UnknownPath, result); + } + + [Fact] + public void TraversingANonExistingFile_ReturnsUnknownPath() + { + // Arrange + var directory = ManifestDirectory.CreateDirectory("a", Array.Empty()); + + // Act + var result = directory.Traverse("missing.txt"); + + // Assert + Assert.Equal(ManifestEntry.UnknownPath, result); + } + + [Fact] + public void TraversingWithDot_ReturnsSelf() + { + // Arrange + var directory = ManifestDirectory.CreateDirectory("a", Array.Empty()); + + // Act + var result = directory.Traverse("."); + + // Assert + Assert.Same(directory, result); + } + + [Fact] + public void TraversingWithDotDot_ReturnsParent() + { + // Arrange + var childDirectory = ManifestDirectory.CreateDirectory("b", Array.Empty()); + var directory = ManifestDirectory.CreateDirectory("a", new[] { childDirectory }); + + // Act + var result = childDirectory.Traverse(".."); + + // Assert + Assert.Equal(directory, result); + } + + [Fact] + public void TraversingRootDirectoryWithDotDot_ReturnsSinkDirectory() + { + // Arrange + var directory = ManifestDirectory.CreateRootDirectory(Array.Empty()); + + // Act + var result = directory.Traverse(".."); + + // Assert + Assert.Equal(ManifestEntry.UnknownPath, result); + } + + [Fact] + public void ScopingAFolderAndTryingToGetAScopedFile_ReturnsSinkDirectory() + { + // Arrange + var directory = ManifestDirectory.CreateRootDirectory(new[] { + ManifestDirectory.CreateDirectory("a", + new[] { new ManifestFile("test1.txt", "text.txt") }), + ManifestDirectory.CreateDirectory("b", + new[] { new ManifestFile("test2.txt", "test2.txt") }) }); + + var newRoot = ((ManifestDirectory)directory.Traverse("a")).ToRootDirectory(); + + // Act + var result = newRoot.Traverse("../b/test.txt"); + + // Assert + Assert.Same(ManifestEntry.UnknownPath, result); + } + + [Theory] + [InlineData("..")] + [InlineData(".")] + [InlineData("file.txt")] + [InlineData("folder")] + public void TraversingUnknownPath_ReturnsSinkDirectory(string path) + { + // Arrange + var directory = ManifestEntry.UnknownPath; + + // Act + var result = directory.Traverse(path); + + // Assert + Assert.Equal(ManifestEntry.UnknownPath, result); + } + } +} diff --git a/src/FileProviders/Embedded/test/Manifest/ManifestParserTests.cs b/src/FileProviders/Embedded/test/Manifest/ManifestParserTests.cs new file mode 100644 index 0000000000..e4edc8bde0 --- /dev/null +++ b/src/FileProviders/Embedded/test/Manifest/ManifestParserTests.cs @@ -0,0 +1,116 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Xunit; + +namespace Microsoft.Extensions.FileProviders.Embedded.Manifest +{ + public class ManifestParserTests + { + [Fact] + public void Parse_UsesDefaultManifestNameForManifest() + { + // Arrange + var assembly = new TestAssembly( + TestEntry.Directory("unused", + TestEntry.File("sample.txt"))); + + // Act + var manifest = ManifestParser.Parse(assembly); + + // Assert + Assert.NotNull(manifest); + } + + [Fact] + public void Parse_FindsManifestWithCustomName() + { + // Arrange + var assembly = new TestAssembly( + TestEntry.Directory("unused", + TestEntry.File("sample.txt")), + manifestName: "Manifest.xml"); + + // Act + var manifest = ManifestParser.Parse(assembly, "Manifest.xml"); + + // Assert + Assert.NotNull(manifest); + } + + [Fact] + public void Parse_ThrowsForEntriesWithDifferentCasing() + { + // Arrange + var assembly = new TestAssembly( + TestEntry.Directory("unused", + TestEntry.File("sample.txt"), + TestEntry.File("SAMPLE.TXT"))); + + // Act & Assert + Assert.Throws(() => ManifestParser.Parse(assembly)); + } + + [Theory] + [MemberData(nameof(MalformedManifests))] + public void Parse_ThrowsForInvalidManifests(string invalidManifest) + { + // Arrange + var assembly = new TestAssembly(invalidManifest); + + // Act & Assert + Assert.Throws(() => ManifestParser.Parse(assembly)); + } + + public static TheoryData MalformedManifests => + new TheoryData + { + "", + "", + "", + "2.0", + "2.0", + @"1.0 +path", + + @"1.0 +", + + @"1.0 +sample.txt", + + @"1.0 +", + + @"1.0 +" + }; + + [Theory] + [MemberData(nameof(ManifestsWithAdditionalData))] + public void Parse_IgnoresAdditionalDataOnFileAndDirectoryNodes(string manifest) + { + // Arrange + var assembly = new TestAssembly(manifest); + + // Act + var result = ManifestParser.Parse(assembly); + + // Assert + Assert.NotNull(result); + } + + public static TheoryData ManifestsWithAdditionalData => + new TheoryData + { + @"1.0 +", + + @"1.0 + +path1234 +" + }; + } +} diff --git a/src/FileProviders/Embedded/test/Manifest/TestEntry.cs b/src/FileProviders/Embedded/test/Manifest/TestEntry.cs new file mode 100644 index 0000000000..aaaf881469 --- /dev/null +++ b/src/FileProviders/Embedded/test/Manifest/TestEntry.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Microsoft.Extensions.FileProviders.Embedded.Manifest +{ + class TestEntry + { + public bool IsFile => ResourcePath != null; + public string Name { get; set; } + public TestEntry[] Children { get; set; } + public string ResourcePath { get; set; } + + public static TestEntry Directory(string name, params TestEntry[] entries) => + new TestEntry() { Name = name, Children = entries }; + + public static TestEntry File(string name, string path = null) => + new TestEntry() { Name = name, ResourcePath = path ?? name }; + + public XElement ToXElement() => IsFile ? + new XElement("File", new XAttribute("Name", Name), new XElement("ResourcePath", ResourcePath)) : + new XElement("Directory", new XAttribute("Name", Name), Children.Select(c => c.ToXElement())); + + public IEnumerable GetFiles() + { + if (IsFile) + { + return Enumerable.Empty(); + } + + var files = Children.Where(c => c.IsFile).ToArray(); + var otherFiles = Children.Where(c => !c.IsFile).SelectMany(d => d.GetFiles()).ToArray(); + + return files.Concat(otherFiles).ToArray(); + } + + } +} diff --git a/src/FileProviders/Embedded/test/ManifestEmbeddedFileProviderTests.cs b/src/FileProviders/Embedded/test/ManifestEmbeddedFileProviderTests.cs new file mode 100644 index 0000000000..a973c9df00 --- /dev/null +++ b/src/FileProviders/Embedded/test/ManifestEmbeddedFileProviderTests.cs @@ -0,0 +1,428 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Linq; +using Microsoft.Extensions.FileProviders.Embedded.Manifest; +using Xunit; + +namespace Microsoft.Extensions.FileProviders +{ + public class ManifestEmbeddedFileProviderTests + { + [Fact] + public void GetFileInfo_CanResolveSimpleFiles() + { + // Arrange + var assembly = new TestAssembly( + TestEntry.Directory("unused", + TestEntry.File("jquery.validate.js"), + TestEntry.File("jquery.min.js"), + TestEntry.File("site.css"))); + + // Act + var provider = new ManifestEmbeddedFileProvider(assembly); + + // Assert + var jqueryValidate = provider.GetFileInfo("jquery.validate.js"); + Assert.True(jqueryValidate.Exists); + Assert.False(jqueryValidate.IsDirectory); + Assert.Equal("jquery.validate.js", jqueryValidate.Name); + Assert.Null(jqueryValidate.PhysicalPath); + Assert.Equal(0, jqueryValidate.Length); + + var jqueryMin = provider.GetFileInfo("jquery.min.js"); + Assert.True(jqueryMin.Exists); + Assert.False(jqueryMin.IsDirectory); + Assert.Equal("jquery.min.js", jqueryMin.Name); + Assert.Null(jqueryMin.PhysicalPath); + Assert.Equal(0, jqueryMin.Length); + + var siteCss = provider.GetFileInfo("site.css"); + Assert.True(siteCss.Exists); + Assert.False(siteCss.IsDirectory); + Assert.Equal("site.css", siteCss.Name); + Assert.Null(siteCss.PhysicalPath); + Assert.Equal(0, siteCss.Length); + } + + [Fact] + public void GetFileInfo_CanResolveFilesInsideAFolder() + { + // Arrange + var assembly = new TestAssembly( + TestEntry.Directory("unused", + TestEntry.Directory("wwwroot", + TestEntry.File("jquery.validate.js"), + TestEntry.File("jquery.min.js"), + TestEntry.File("site.css")))); + + // Act + var provider = new ManifestEmbeddedFileProvider(assembly); + + // Assert + var jqueryValidate = provider.GetFileInfo(Path.Combine("wwwroot", "jquery.validate.js")); + Assert.True(jqueryValidate.Exists); + Assert.False(jqueryValidate.IsDirectory); + Assert.Equal("jquery.validate.js", jqueryValidate.Name); + Assert.Null(jqueryValidate.PhysicalPath); + Assert.Equal(0, jqueryValidate.Length); + + var jqueryMin = provider.GetFileInfo(Path.Combine("wwwroot", "jquery.min.js")); + Assert.True(jqueryMin.Exists); + Assert.False(jqueryMin.IsDirectory); + Assert.Equal("jquery.min.js", jqueryMin.Name); + Assert.Null(jqueryMin.PhysicalPath); + Assert.Equal(0, jqueryMin.Length); + + var siteCss = provider.GetFileInfo(Path.Combine("wwwroot", "site.css")); + Assert.True(siteCss.Exists); + Assert.False(siteCss.IsDirectory); + Assert.Equal("site.css", siteCss.Name); + Assert.Null(siteCss.PhysicalPath); + Assert.Equal(0, siteCss.Length); + } + + [Fact] + public void GetFileInfo_ResolveNonExistingFile_ReturnsNotFoundFileInfo() + { + // Arrange + var assembly = new TestAssembly( + TestEntry.Directory("unused", + TestEntry.Directory("wwwroot", + TestEntry.File("jquery.validate.js"), + TestEntry.File("jquery.min.js"), + TestEntry.File("site.css")))); + + var provider = new ManifestEmbeddedFileProvider(assembly); + + // Act + var file = provider.GetFileInfo("some/non/existing/file.txt"); + + // Assert + Assert.IsType(file); + } + + [Fact] + public void GetFileInfo_ResolveNonExistingDirectory_ReturnsNotFoundFileInfo() + { + // Arrange + var assembly = new TestAssembly( + TestEntry.Directory("unused", + TestEntry.Directory("wwwroot", + TestEntry.File("jquery.validate.js"), + TestEntry.File("jquery.min.js"), + TestEntry.File("site.css")))); + + var provider = new ManifestEmbeddedFileProvider(assembly); + + // Act + var file = provider.GetFileInfo("some"); + + // Assert + Assert.IsType(file); + } + + [Fact] + public void GetFileInfo_ResolveExistingDirectory_ReturnsNotFoundFileInfo() + { + // Arrange + var assembly = new TestAssembly( + TestEntry.Directory("unused", + TestEntry.Directory("wwwroot", + TestEntry.File("jquery.validate.js"), + TestEntry.File("jquery.min.js"), + TestEntry.File("site.css")))); + + var provider = new ManifestEmbeddedFileProvider(assembly); + + // Act + var file = provider.GetFileInfo("wwwroot"); + + // Assert + Assert.IsType(file); + } + + [Theory] + [InlineData("WWWROOT", "JQUERY.VALIDATE.JS")] + [InlineData("WwWRoOT", "JQuERY.VALiDATE.js")] + public void GetFileInfo_ResolvesFiles_WithDifferentCasing(string folder, string file) + { + // Arrange + var assembly = new TestAssembly( + TestEntry.Directory("unused", + TestEntry.Directory("wwwroot", + TestEntry.File("jquery.validate.js"), + TestEntry.File("jquery.min.js"), + TestEntry.File("site.css")))); + + // Act + var provider = new ManifestEmbeddedFileProvider(assembly); + + // Assert + var jqueryValidate = provider.GetFileInfo(Path.Combine(folder, file)); + Assert.True(jqueryValidate.Exists); + Assert.False(jqueryValidate.IsDirectory); + Assert.Equal("jquery.validate.js", jqueryValidate.Name); + Assert.Null(jqueryValidate.PhysicalPath); + Assert.Equal(0, jqueryValidate.Length); + } + + [Fact] + public void GetFileInfo_AllowsLeadingDots_OnThePath() + { + // Arrange + var assembly = new TestAssembly( + TestEntry.Directory("unused", + TestEntry.Directory("wwwroot", + TestEntry.File("jquery.validate.js"), + TestEntry.File("jquery.min.js"), + TestEntry.File("site.css")))); + + // Act + var provider = new ManifestEmbeddedFileProvider(assembly); + + // Assert + var jqueryValidate = provider.GetFileInfo(Path.Combine(".", "wwwroot", "jquery.validate.js")); + Assert.True(jqueryValidate.Exists); + Assert.False(jqueryValidate.IsDirectory); + Assert.Equal("jquery.validate.js", jqueryValidate.Name); + Assert.Null(jqueryValidate.PhysicalPath); + Assert.Equal(0, jqueryValidate.Length); + } + + [Fact] + public void GetFileInfo_EscapingFromTheRootFolder_ReturnsNotFound() + { + // Arrange + var assembly = new TestAssembly( + TestEntry.Directory("unused", + TestEntry.Directory("wwwroot", + TestEntry.File("jquery.validate.js"), + TestEntry.File("jquery.min.js"), + TestEntry.File("site.css")))); + + // Act + var provider = new ManifestEmbeddedFileProvider(assembly); + + // Assert + var jqueryValidate = provider.GetFileInfo(Path.Combine("..", "wwwroot", "jquery.validate.js")); + Assert.IsType(jqueryValidate); + } + + [Theory] + [InlineData("wwwroot/jquery?validate.js")] + [InlineData("wwwroot/jquery*validate.js")] + [InlineData("wwwroot/jquery:validate.js")] + [InlineData("wwwroot/jqueryvalidate.js")] + [InlineData("wwwroot/jquery\0validate.js")] + public void GetFileInfo_ReturnsNotFoundfileInfo_ForPathsWithInvalidCharacters(string path) + { + // Arrange + var assembly = new TestAssembly( + TestEntry.Directory("unused", + TestEntry.Directory("wwwroot", + TestEntry.File("jquery.validate.js"), + TestEntry.File("jquery.min.js"), + TestEntry.File("site.css")))); + + // Act + var provider = new ManifestEmbeddedFileProvider(assembly); + + // Assert + var file = provider.GetFileInfo(path); + Assert.IsType(file); + Assert.Equal(path, file.Name); + } + + [Fact] + public void GetDirectoryContents_CanEnumerateExistingFolders() + { + // Arrange + var assembly = new TestAssembly( + TestEntry.Directory("unused", + TestEntry.Directory("wwwroot", + TestEntry.File("jquery.validate.js"), + TestEntry.File("jquery.min.js"), + TestEntry.File("site.css")))); + + var provider = new ManifestEmbeddedFileProvider(assembly); + + var expectedContents = new[] + { + CreateTestFileInfo("jquery.validate.js"), + CreateTestFileInfo("jquery.min.js"), + CreateTestFileInfo("site.css") + }; + + // Act + var contents = provider.GetDirectoryContents("wwwroot").ToArray(); + + // Assert + Assert.Equal(expectedContents, contents, FileInfoComparer.Instance); + } + + [Fact] + public void GetDirectoryContents_EnumeratesOnlyAGivenLevel() + { + // Arrange + var assembly = new TestAssembly( + TestEntry.Directory("unused", + TestEntry.Directory("wwwroot", + TestEntry.File("jquery.validate.js"), + TestEntry.File("jquery.min.js"), + TestEntry.File("site.css")))); + + var provider = new ManifestEmbeddedFileProvider(assembly); + + var expectedContents = new[] + { + CreateTestFileInfo("wwwroot", isDirectory: true) + }; + + // Act + var contents = provider.GetDirectoryContents(".").ToArray(); + + // Assert + Assert.Equal(expectedContents, contents, FileInfoComparer.Instance); + } + + [Fact] + public void GetDirectoryContents_EnumeratesFilesAndDirectoriesOnAGivenPath() + { + // Arrange + var assembly = new TestAssembly( + TestEntry.Directory("unused", + TestEntry.Directory("wwwroot"), + TestEntry.File("site.css"))); + + var provider = new ManifestEmbeddedFileProvider(assembly); + + var expectedContents = new[] + { + CreateTestFileInfo("wwwroot", isDirectory: true), + CreateTestFileInfo("site.css") + }; + + // Act + var contents = provider.GetDirectoryContents(".").ToArray(); + + // Assert + Assert.Equal(expectedContents, contents, FileInfoComparer.Instance); + } + + [Fact] + public void GetDirectoryContents_ReturnsNoEntries_ForNonExistingDirectories() + { + // Arrange + var assembly = new TestAssembly( + TestEntry.Directory("unused", + TestEntry.Directory("wwwroot"), + TestEntry.File("site.css"))); + + var provider = new ManifestEmbeddedFileProvider(assembly); + + // Act + var contents = provider.GetDirectoryContents("non-existing"); + + // Assert + Assert.IsType(contents); + } + + [Fact] + public void GetDirectoryContents_ReturnsNoEntries_ForFilePaths() + { + // Arrange + var assembly = new TestAssembly( + TestEntry.Directory("unused", + TestEntry.Directory("wwwroot"), + TestEntry.File("site.css"))); + + var provider = new ManifestEmbeddedFileProvider(assembly); + + // Act + var contents = provider.GetDirectoryContents("site.css"); + + // Assert + Assert.IsType(contents); + } + + [Theory] + [InlineData("wwwro*t")] + [InlineData("wwwro?t")] + [InlineData("wwwro:t")] + [InlineData("wwwrot")] + [InlineData("wwwro\0t")] + public void GetDirectoryContents_ReturnsNotFoundDirectoryContents_ForPathsWithInvalidCharacters(string path) + { + // Arrange + var assembly = new TestAssembly( + TestEntry.Directory("unused", + TestEntry.Directory("wwwroot", + TestEntry.File("jquery.validate.js"), + TestEntry.File("jquery.min.js"), + TestEntry.File("site.css")))); + + // Act + var provider = new ManifestEmbeddedFileProvider(assembly); + + // Assert + var directory = provider.GetDirectoryContents(path); + Assert.IsType(directory); + } + + [Fact] + public void Contructor_CanScopeManifestToAFolder() + { + // Arrange + var assembly = new TestAssembly( + TestEntry.Directory("unused", + TestEntry.Directory("wwwroot", + TestEntry.File("jquery.validate.js")), + TestEntry.File("site.css"))); + + var provider = new ManifestEmbeddedFileProvider(assembly); + var scopedProvider = new ManifestEmbeddedFileProvider(assembly, provider.Manifest.Scope("wwwroot"), DateTimeOffset.UtcNow); + + // Act + var jqueryValidate = scopedProvider.GetFileInfo("jquery.validate.js"); + + // Assert + Assert.True(jqueryValidate.Exists); + Assert.False(jqueryValidate.IsDirectory); + Assert.Equal("jquery.validate.js", jqueryValidate.Name); + Assert.Null(jqueryValidate.PhysicalPath); + Assert.Equal(0, jqueryValidate.Length); + } + + [Theory] + [InlineData("wwwroot/jquery.validate.js")] + [InlineData("../wwwroot/jquery.validate.js")] + [InlineData("site.css")] + [InlineData("../site.css")] + public void ScopedFileProvider_DoesNotReturnFilesOutOfScope(string path) + { + // Arrange + var assembly = new TestAssembly( + TestEntry.Directory("unused", + TestEntry.Directory("wwwroot", + TestEntry.File("jquery.validate.js")), + TestEntry.File("site.css"))); + + var provider = new ManifestEmbeddedFileProvider(assembly); + var scopedProvider = new ManifestEmbeddedFileProvider(assembly, provider.Manifest.Scope("wwwroot"), DateTimeOffset.UtcNow); + + // Act + var jqueryValidate = scopedProvider.GetFileInfo(path); + + // Assert + Assert.IsType(jqueryValidate); + } + + private IFileInfo CreateTestFileInfo(string name, bool isDirectory = false) => + new TestFileInfo(name, isDirectory); + } +} diff --git a/src/FileProviders/Embedded/test/Microsoft.Extensions.FileProviders.Embedded.Tests.csproj b/src/FileProviders/Embedded/test/Microsoft.Extensions.FileProviders.Embedded.Tests.csproj new file mode 100644 index 0000000000..8703c9c508 --- /dev/null +++ b/src/FileProviders/Embedded/test/Microsoft.Extensions.FileProviders.Embedded.Tests.csproj @@ -0,0 +1,15 @@ + + + + $(StandardTestTfms) + + + + + + + + + + + diff --git a/src/FileProviders/Embedded/test/Resources/File.txt b/src/FileProviders/Embedded/test/Resources/File.txt new file mode 100644 index 0000000000..d498f37006 --- /dev/null +++ b/src/FileProviders/Embedded/test/Resources/File.txt @@ -0,0 +1 @@ +Resources-Hello \ No newline at end of file diff --git a/src/FileProviders/Embedded/test/Resources/ResourcesInSubdirectory/File3.txt b/src/FileProviders/Embedded/test/Resources/ResourcesInSubdirectory/File3.txt new file mode 100644 index 0000000000..8651decea6 --- /dev/null +++ b/src/FileProviders/Embedded/test/Resources/ResourcesInSubdirectory/File3.txt @@ -0,0 +1 @@ +Hello3 diff --git a/src/FileProviders/Embedded/test/TestAssembly.cs b/src/FileProviders/Embedded/test/TestAssembly.cs new file mode 100644 index 0000000000..4917bd60ed --- /dev/null +++ b/src/FileProviders/Embedded/test/TestAssembly.cs @@ -0,0 +1,69 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Xml; +using System.Xml.Linq; +using Microsoft.Extensions.FileProviders.Embedded.Manifest; + +namespace Microsoft.Extensions.FileProviders +{ + internal class TestAssembly : Assembly + { + public TestAssembly(string manifest, string manifestName = "Microsoft.Extensions.FileProviders.Embedded.Manifest.xml") + { + ManifestStream = new MemoryStream(); + using (var writer = new StreamWriter(ManifestStream, Encoding.UTF8, 1024, leaveOpen: true)) + { + writer.Write(manifest); + } + + ManifestStream.Seek(0, SeekOrigin.Begin); + ManifestName = manifestName; + } + + public TestAssembly(TestEntry entry, string manifestName = "Microsoft.Extensions.FileProviders.Embedded.Manifest.xml") + { + ManifestName = manifestName; + + var manifest = new XDocument( + new XDeclaration("1.0", "utf-8", "yes"), + new XElement("Manifest", + new XElement("ManifestVersion", "1.0"), + new XElement("FileSystem", entry.Children.Select(c => c.ToXElement())))); + + ManifestStream = new MemoryStream(); + using (var writer = XmlWriter.Create(ManifestStream, new XmlWriterSettings { CloseOutput = false })) + { + manifest.WriteTo(writer); + } + + ManifestStream.Seek(0, SeekOrigin.Begin); + Files = entry.GetFiles().Select(f => f.ResourcePath).ToArray(); + } + + public string ManifestName { get; } + public MemoryStream ManifestStream { get; private set; } + public string[] Files { get; private set; } + + public override Stream GetManifestResourceStream(string name) + { + if (string.Equals(ManifestName, name)) + { + return ManifestStream; + } + + return Files.Contains(name) ? Stream.Null : null; + } + + public override string Location => null; + + public override AssemblyName GetName() + { + return new AssemblyName("TestAssembly"); + } + } +} diff --git a/src/FileProviders/Embedded/test/TestFileInfo.cs b/src/FileProviders/Embedded/test/TestFileInfo.cs new file mode 100644 index 0000000000..d410a3b5e7 --- /dev/null +++ b/src/FileProviders/Embedded/test/TestFileInfo.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; + +namespace Microsoft.Extensions.FileProviders +{ + internal class TestFileInfo : IFileInfo + { + private readonly string _name; + private readonly bool _isDirectory; + + public TestFileInfo(string name, bool isDirectory) + { + _name = name; + _isDirectory = isDirectory; + } + + public bool Exists => true; + + public long Length => _isDirectory ? -1 : 0; + + public string PhysicalPath => null; + + public string Name => _name; + + public DateTimeOffset LastModified => throw new NotImplementedException(); + + public bool IsDirectory => _isDirectory; + + public Stream CreateReadStream() => Stream.Null; + } +} diff --git a/src/FileProviders/Embedded/test/sub/File2.txt b/src/FileProviders/Embedded/test/sub/File2.txt new file mode 100644 index 0000000000..e8ecfad884 --- /dev/null +++ b/src/FileProviders/Embedded/test/sub/File2.txt @@ -0,0 +1 @@ +Hello2 diff --git a/src/FileProviders/Embedded/test/sub/dir/File3.txt b/src/FileProviders/Embedded/test/sub/dir/File3.txt new file mode 100644 index 0000000000000000000000000000000000000000..49cc8ef0e116cef009fe0bd72473a964bbd07f9b GIT binary patch literal 6 NcmezWkC%aq0RRg=0u=xN literal 0 HcmV?d00001 diff --git a/src/FileProviders/Manifest.MSBuildTask/src/EmbeddedItem.cs b/src/FileProviders/Manifest.MSBuildTask/src/EmbeddedItem.cs new file mode 100644 index 0000000000..c2dbd58ed2 --- /dev/null +++ b/src/FileProviders/Manifest.MSBuildTask/src/EmbeddedItem.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Extensions.FileProviders.Embedded.Manifest.Task +{ + public class EmbeddedItem : IEquatable + { + public string ManifestFilePath { get; set; } + + public string AssemblyResourceName { get; set; } + + public bool Equals(EmbeddedItem other) => + string.Equals(ManifestFilePath, other?.ManifestFilePath, StringComparison.Ordinal) && + string.Equals(AssemblyResourceName, other?.AssemblyResourceName, StringComparison.Ordinal); + + public override bool Equals(object obj) => Equals(obj as EmbeddedItem); + public override int GetHashCode() => ManifestFilePath.GetHashCode() ^ AssemblyResourceName.GetHashCode(); + } +} diff --git a/src/FileProviders/Manifest.MSBuildTask/src/Entry.cs b/src/FileProviders/Manifest.MSBuildTask/src/Entry.cs new file mode 100644 index 0000000000..40c815fde4 --- /dev/null +++ b/src/FileProviders/Manifest.MSBuildTask/src/Entry.cs @@ -0,0 +1,120 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.Internal +{ + /// + /// This type is for internal uses only and is not meant to be consumed by any other library. + /// + [DebuggerDisplay("{Name,nq}")] + public class Entry : IEquatable + { + public bool IsFile { get; private set; } + + public string Name { get; private set; } + + public string AssemblyResourceName { get; private set; } + + public ISet Children { get; } = new SortedSet(NameComparer.Instance); + + public static Entry Directory(string name) => + new Entry { Name = name }; + + public static Entry File(string name, string assemblyResourceName) => + new Entry { Name = name, AssemblyResourceName = assemblyResourceName, IsFile = true }; + + internal void AddChild(Entry child) + { + if (IsFile) + { + throw new InvalidOperationException("Tried to add children to a file."); + } + + if (Children.Contains(child)) + { + throw new InvalidOperationException($"An item with the name '{child.Name}' already exists."); + } + + Children.Add(child); + } + + internal Entry GetDirectory(string currentSegment) + { + if (IsFile) + { + throw new InvalidOperationException("Tried to get a directory from a file."); + } + + foreach (var child in Children) + { + if (child.HasName(currentSegment)) + { + if (child.IsFile) + { + throw new InvalidOperationException("Tried to find a directory but found a file instead"); + } + else + { + return child; + } + } + } + + return null; + } + + public bool Equals(Entry other) + { + if (other == null || !other.HasName(Name) || other.IsFile != IsFile) + { + return false; + } + + if (IsFile) + { + return string.Equals(other.AssemblyResourceName, AssemblyResourceName, StringComparison.Ordinal); + } + else + { + return SameChildren(Children, other.Children); + } + } + + private bool HasName(string currentSegment) + { + return string.Equals(Name, currentSegment, StringComparison.Ordinal); + } + + private bool SameChildren(ISet left, ISet right) + { + if (left.Count != right.Count) + { + return false; + } + + var le = left.GetEnumerator(); + var re = right.GetEnumerator(); + while (le.MoveNext() && re.MoveNext()) + { + if (!le.Current.Equals(re.Current)) + { + return false; + } + } + + return true; + } + + private class NameComparer : IComparer + { + public static NameComparer Instance { get; } = new NameComparer(); + + public int Compare(Entry x, Entry y) => + string.Compare(x?.Name, y?.Name, StringComparison.Ordinal); + } + } +} diff --git a/src/FileProviders/Manifest.MSBuildTask/src/GenerateEmbeddedResourcesManifest.cs b/src/FileProviders/Manifest.MSBuildTask/src/GenerateEmbeddedResourcesManifest.cs new file mode 100644 index 0000000000..3a62d3d5e3 --- /dev/null +++ b/src/FileProviders/Manifest.MSBuildTask/src/GenerateEmbeddedResourcesManifest.cs @@ -0,0 +1,104 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; +using Microsoft.Build.Framework; + +namespace Microsoft.Extensions.FileProviders.Embedded.Manifest.Task +{ + /// + /// Task for generating a manifest file out of the embedded resources in an + /// assembly. + /// + public class GenerateEmbeddedResourcesManifest : Microsoft.Build.Utilities.Task + { + private const string LogicalName = "LogicalName"; + private const string ManifestResourceName = "ManifestResourceName"; + private const string TargetPath = "TargetPath"; + + [Required] + public ITaskItem[] EmbeddedFiles { get; set; } + + [Required] + public string ManifestFile { get; set; } + + /// + public override bool Execute() + { + var processedItems = CreateEmbeddedItems(EmbeddedFiles); + + var manifest = BuildManifest(processedItems); + + var document = manifest.ToXmlDocument(); + + var settings = new XmlWriterSettings() + { + Encoding = Encoding.UTF8, + CloseOutput = true + }; + + using (var xmlWriter = GetXmlWriter(settings)) + { + document.WriteTo(xmlWriter); + } + + return true; + } + + protected virtual XmlWriter GetXmlWriter(XmlWriterSettings settings) + { + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + var fileStream = new FileStream(ManifestFile, FileMode.Create); + return XmlWriter.Create(fileStream, settings); + } + + public EmbeddedItem[] CreateEmbeddedItems(params ITaskItem[] items) + { + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } + + return items.Select(er => new EmbeddedItem + { + ManifestFilePath = GetManifestPath(er), + AssemblyResourceName = GetAssemblyResourceName(er) + }).ToArray(); + } + + public Manifest BuildManifest(EmbeddedItem[] processedItems) + { + if (processedItems == null) + { + throw new ArgumentNullException(nameof(processedItems)); + } + + var manifest = new Manifest(); + foreach (var item in processedItems) + { + manifest.AddElement(item.ManifestFilePath, item.AssemblyResourceName); + } + + return manifest; + } + + private string GetManifestPath(ITaskItem taskItem) => string.Equals(taskItem.GetMetadata(LogicalName), taskItem.GetMetadata(ManifestResourceName)) ? + taskItem.GetMetadata(TargetPath) : + NormalizePath(taskItem.GetMetadata(LogicalName)); + + private string GetAssemblyResourceName(ITaskItem taskItem) => string.Equals(taskItem.GetMetadata(LogicalName), taskItem.GetMetadata(ManifestResourceName)) ? + taskItem.GetMetadata(ManifestResourceName) : + taskItem.GetMetadata(LogicalName); + + private string NormalizePath(string path) => Path.DirectorySeparatorChar == '\\' ? + path.Replace("/", "\\") : path.Replace("\\", "/"); + } +} diff --git a/src/FileProviders/Manifest.MSBuildTask/src/Manifest.cs b/src/FileProviders/Manifest.MSBuildTask/src/Manifest.cs new file mode 100644 index 0000000000..86e99477ff --- /dev/null +++ b/src/FileProviders/Manifest.MSBuildTask/src/Manifest.cs @@ -0,0 +1,85 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.Internal; + +namespace Microsoft.Extensions.FileProviders.Embedded.Manifest.Task +{ + public class Manifest + { + public Entry Root { get; set; } = Entry.Directory(""); + + public void AddElement(string originalPath, string assemblyResourceName) + { + if (originalPath == null) + { + throw new System.ArgumentNullException(nameof(originalPath)); + } + + if (assemblyResourceName == null) + { + throw new System.ArgumentNullException(nameof(assemblyResourceName)); + } + + var paths = originalPath.Split(Path.DirectorySeparatorChar); + var current = Root; + for (int i = 0; i < paths.Length - 1; i++) + { + var currentSegment = paths[i]; + var next = current.GetDirectory(currentSegment); + if (next == null) + { + next = Entry.Directory(currentSegment); + current.AddChild(next); + } + current = next; + } + + current.AddChild(Entry.File(paths[paths.Length - 1], assemblyResourceName)); + } + + public XDocument ToXmlDocument() + { + var document = new XDocument(new XDeclaration("1.0", "utf-8", "yes")); + var root = new XElement(ElementNames.Root, + new XElement(ElementNames.ManifestVersion, "1.0"), + new XElement(ElementNames.FileSystem, + Root.Children.Select(e => BuildNode(e)))); + + document.Add(root); + + return document; + } + + private XElement BuildNode(Entry entry) + { + if (entry.IsFile) + { + return new XElement(ElementNames.File, + new XAttribute(ElementNames.Name, entry.Name), + new XElement(ElementNames.ResourcePath, entry.AssemblyResourceName)); + } + else + { + var directory = new XElement(ElementNames.Directory, new XAttribute(ElementNames.Name, entry.Name)); + directory.Add(entry.Children.Select(c => BuildNode(c))); + return directory; + } + } + + private class ElementNames + { + public static readonly string Directory = "Directory"; + public static readonly string Name = "Name"; + public static readonly string FileSystem = "FileSystem"; + public static readonly string Root = "Manifest"; + public static readonly string File = "File"; + public static readonly string ResourcePath = "ResourcePath"; + public static readonly string ManifestVersion = "ManifestVersion"; + } + } +} diff --git a/src/FileProviders/Manifest.MSBuildTask/src/Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.csproj b/src/FileProviders/Manifest.MSBuildTask/src/Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.csproj new file mode 100644 index 0000000000..70784650de --- /dev/null +++ b/src/FileProviders/Manifest.MSBuildTask/src/Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.csproj @@ -0,0 +1,25 @@ + + + + MSBuild task to generate a manifest that can be used by Microsoft.Extensions.FileProviders.Embedded to preserve + metadata of the files embedded in the assembly at compilation time. + netstandard1.5;net461 + false + false + false + false + + + + + + + + + + + + + + + diff --git a/src/FileProviders/Manifest.MSBuildTask/test/GenerateEmbeddedResourcesManifestTest.cs b/src/FileProviders/Manifest.MSBuildTask/test/GenerateEmbeddedResourcesManifestTest.cs new file mode 100644 index 0000000000..c7285913af --- /dev/null +++ b/src/FileProviders/Manifest.MSBuildTask/test/GenerateEmbeddedResourcesManifestTest.cs @@ -0,0 +1,388 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; +using System.Xml.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.Internal; +using Xunit; + +namespace Microsoft.Extensions.FileProviders.Embedded.Manifest.Task +{ + public class GenerateEmbeddedResourcesManifestTest + { + [Fact] + public void CreateEmbeddedItems_MapsMetadataFromEmbeddedResources_UsesTheTargetPath() + { + // Arrange + var task = new TestGenerateEmbeddedResourcesManifest(); + var embeddedFiles = CreateEmbeddedResource( + CreateMetadata(@"lib\js\jquery.validate.js")); + + var expectedItems = new[] + { + CreateEmbeddedItem(@"lib\js\jquery.validate.js","lib.js.jquery.validate.js") + }; + + // Act + var embeddedItems = task.CreateEmbeddedItems(embeddedFiles); + + // Assert + Assert.Equal(expectedItems, embeddedItems); + } + + [Fact] + public void CreateEmbeddedItems_MapsMetadataFromEmbeddedResources_WithLogicalName() + { + // Arrange + var task = new TestGenerateEmbeddedResourcesManifest(); + var DirectorySeparator = (Path.DirectorySeparatorChar == '\\' ? '/' : '\\'); + var embeddedFiles = CreateEmbeddedResource( + CreateMetadata("site.css", null, "site.css"), + CreateMetadata("lib/jquery.validate.js", null, $"dist{DirectorySeparator}jquery.validate.js")); + + var expectedItems = new[] + { + CreateEmbeddedItem("site.css","site.css"), + CreateEmbeddedItem(Path.Combine("dist","jquery.validate.js"),$"dist{DirectorySeparator}jquery.validate.js") + }; + + // Act + var embeddedItems = task.CreateEmbeddedItems(embeddedFiles); + + // Assert + Assert.Equal(expectedItems, embeddedItems); + } + + [Fact] + public void BuildManifest_CanCreatesManifest_ForTopLevelFiles() + { + // Arrange + var task = new TestGenerateEmbeddedResourcesManifest(); + var embeddedFiles = CreateEmbeddedResource( + CreateMetadata("jquery.validate.js"), + CreateMetadata("jquery.min.js"), + CreateMetadata("Site.css")); + + var manifestFiles = task.CreateEmbeddedItems(embeddedFiles); + + var expectedManifest = new Manifest() + { + Root = Entry.Directory("").AddRange( + Entry.File("jquery.validate.js", "jquery.validate.js"), + Entry.File("jquery.min.js", "jquery.min.js"), + Entry.File("Site.css", "Site.css")) + }; + + // Act + var manifest = task.BuildManifest(manifestFiles); + + // Assert + Assert.Equal(expectedManifest, manifest, ManifestComparer.Instance); + } + + [Fact] + public void BuildManifest_CanCreatesManifest_ForFilesWithinAFolder() + { + // Arrange + var task = new TestGenerateEmbeddedResourcesManifest(); + var embeddedFiles = CreateEmbeddedResource( + CreateMetadata(Path.Combine("wwwroot", "js", "jquery.validate.js")), + CreateMetadata(Path.Combine("wwwroot", "js", "jquery.min.js")), + CreateMetadata(Path.Combine("wwwroot", "css", "Site.css")), + CreateMetadata(Path.Combine("Areas", "Identity", "Views", "Account", "Index.cshtml"))); + + var manifestFiles = task.CreateEmbeddedItems(embeddedFiles); + + var expectedManifest = new Manifest() + { + Root = Entry.Directory("").AddRange( + Entry.Directory("wwwroot").AddRange( + Entry.Directory("js").AddRange( + Entry.File("jquery.validate.js", "wwwroot.js.jquery.validate.js"), + Entry.File("jquery.min.js", "wwwroot.js.jquery.min.js")), + Entry.Directory("css").AddRange( + Entry.File("Site.css", "wwwroot.css.Site.css"))), + Entry.Directory("Areas").AddRange( + Entry.Directory("Identity").AddRange( + Entry.Directory("Views").AddRange( + Entry.Directory("Account").AddRange( + Entry.File("Index.cshtml", "Areas.Identity.Views.Account.Index.cshtml")))))) + }; + + // Act + var manifest = task.BuildManifest(manifestFiles); + + // Assert + Assert.Equal(expectedManifest, manifest, ManifestComparer.Instance); + } + + [Fact] + public void BuildManifest_RespectsEntriesWithLogicalName() + { + // Arrange + var task = new TestGenerateEmbeddedResourcesManifest(); + var embeddedFiles = CreateEmbeddedResource( + CreateMetadata("jquery.validate.js", null, @"wwwroot\lib\js\jquery.validate.js"), + CreateMetadata("jquery.min.js", null, @"wwwroot\lib/js\jquery.min.js"), + CreateMetadata("Site.css", null, "wwwroot/lib/css/site.css")); + var manifestFiles = task.CreateEmbeddedItems(embeddedFiles); + + var expectedManifest = new Manifest() + { + Root = Entry.Directory("").AddRange( + Entry.Directory("wwwroot").AddRange( + Entry.Directory("lib").AddRange( + Entry.Directory("js").AddRange( + Entry.File("jquery.validate.js", @"wwwroot\lib\js\jquery.validate.js"), + Entry.File("jquery.min.js", @"wwwroot\lib/js\jquery.min.js")), + Entry.Directory("css").AddRange( + Entry.File("site.css", "wwwroot/lib/css/site.css"))))) + }; + + // Act + var manifest = task.BuildManifest(manifestFiles); + + // Assert + Assert.Equal(expectedManifest, manifest, ManifestComparer.Instance); + } + + [Fact] + public void BuildManifest_SupportsFilesAndFoldersWithDifferentCasing() + { + // Arrange + var task = new TestGenerateEmbeddedResourcesManifest(); + var embeddedFiles = CreateEmbeddedResource( + CreateMetadata(Path.Combine("A", "b", "c.txt")), + CreateMetadata(Path.Combine("A", "B", "c.txt")), + CreateMetadata(Path.Combine("A", "B", "C.txt")), + CreateMetadata(Path.Combine("A", "b", "C.txt")), + CreateMetadata(Path.Combine("A", "d")), + CreateMetadata(Path.Combine("A", "D", "e.txt"))); + + var manifestFiles = task.CreateEmbeddedItems(embeddedFiles); + + var expectedManifest = new Manifest() + { + Root = Entry.Directory("").AddRange( + Entry.Directory("A").AddRange( + Entry.Directory("b").AddRange( + Entry.File("c.txt", @"A.b.c.txt"), + Entry.File("C.txt", @"A.b.C.txt")), + Entry.Directory("B").AddRange( + Entry.File("c.txt", @"A.B.c.txt"), + Entry.File("C.txt", @"A.B.C.txt")), + Entry.Directory("D").AddRange( + Entry.File("e.txt", "A.D.e.txt")), + Entry.File("d", "A.d"))) + }; + + // Act + var manifest = task.BuildManifest(manifestFiles); + + // Assert + Assert.Equal(expectedManifest, manifest, ManifestComparer.Instance); + } + + [Fact] + public void BuildManifest_ThrowsInvalidOperationException_WhenTryingToAddAFileWithTheSameNameAsAFolder() + { + // Arrange + var task = new TestGenerateEmbeddedResourcesManifest(); + var embeddedFiles = CreateEmbeddedResource( + CreateMetadata(Path.Combine("A", "b", "c.txt")), + CreateMetadata(Path.Combine("A", "b"))); + + var manifestFiles = task.CreateEmbeddedItems(embeddedFiles); + + // Act & Assert + Assert.Throws(() => task.BuildManifest(manifestFiles)); + } + + [Fact] + public void BuildManifest_ThrowsInvalidOperationException_WhenTryingToAddAFolderWithTheSameNameAsAFile() + { + // Arrange + var task = new TestGenerateEmbeddedResourcesManifest(); + var embeddedFiles = CreateEmbeddedResource( + CreateMetadata(Path.Combine("A", "b")), + CreateMetadata(Path.Combine("A", "b", "c.txt"))); + + var manifestFiles = task.CreateEmbeddedItems(embeddedFiles); + + // Act & Assert + Assert.Throws(() => task.BuildManifest(manifestFiles)); + } + + [Fact] + public void ToXmlDocument_GeneratesTheCorrectXmlDocument() + { + // Arrange + var manifest = new Manifest() + { + Root = Entry.Directory("").AddRange( + Entry.Directory("A").AddRange( + Entry.Directory("b").AddRange( + Entry.File("c.txt", @"A.b.c.txt"), + Entry.File("C.txt", @"A.b.C.txt")), + Entry.Directory("B").AddRange( + Entry.File("c.txt", @"A.B.c.txt"), + Entry.File("C.txt", @"A.B.C.txt")), + Entry.Directory("D").AddRange( + Entry.File("e.txt", "A.D.e.txt")), + Entry.File("d", "A.d"))) + }; + + var expectedDocument = new XDocument( + new XDeclaration("1.0", "utf-8", "yes"), + new XElement("Manifest", + new XElement("ManifestVersion", "1.0"), + new XElement("FileSystem", + new XElement("Directory", new XAttribute("Name", "A"), + new XElement("Directory", new XAttribute("Name", "B"), + new XElement("File", new XAttribute("Name", "C.txt"), new XElement("ResourcePath", "A.B.C.txt")), + new XElement("File", new XAttribute("Name", "c.txt"), new XElement("ResourcePath", "A.B.c.txt"))), + new XElement("Directory", new XAttribute("Name", "D"), + new XElement("File", new XAttribute("Name", "e.txt"), new XElement("ResourcePath", "A.D.e.txt"))), + new XElement("Directory", new XAttribute("Name", "b"), + new XElement("File", new XAttribute("Name", "C.txt"), new XElement("ResourcePath", "A.b.C.txt")), + new XElement("File", new XAttribute("Name", "c.txt"), new XElement("ResourcePath", "A.b.c.txt"))), + new XElement("File", new XAttribute("Name", "d"), new XElement("ResourcePath", "A.d")))))); + + // Act + var document = manifest.ToXmlDocument(); + + // Assert + Assert.Equal(expectedDocument.ToString(), document.ToString()); + } + + [Fact] + public void Execute_WritesManifest_ToOutputFile() + { + // Arrange + var task = new TestGenerateEmbeddedResourcesManifest(); + var embeddedFiles = CreateEmbeddedResource( + CreateMetadata(Path.Combine("A", "b", "c.txt")), + CreateMetadata(Path.Combine("A", "B", "c.txt")), + CreateMetadata(Path.Combine("A", "B", "C.txt")), + CreateMetadata(Path.Combine("A", "b", "C.txt")), + CreateMetadata(Path.Combine("A", "d")), + CreateMetadata(Path.Combine("A", "D", "e.txt"))); + + task.EmbeddedFiles = embeddedFiles; + task.ManifestFile = Path.Combine("obj", "debug", "netstandard2.0"); + + var expectedDocument = new XDocument( + new XDeclaration("1.0", "utf-8", "yes"), + new XElement("Manifest", + new XElement("ManifestVersion", "1.0"), + new XElement("FileSystem", + new XElement("Directory", new XAttribute("Name", "A"), + new XElement("Directory", new XAttribute("Name", "B"), + new XElement("File", new XAttribute("Name", "C.txt"), new XElement("ResourcePath", "A.B.C.txt")), + new XElement("File", new XAttribute("Name", "c.txt"), new XElement("ResourcePath", "A.B.c.txt"))), + new XElement("Directory", new XAttribute("Name", "D"), + new XElement("File", new XAttribute("Name", "e.txt"), new XElement("ResourcePath", "A.D.e.txt"))), + new XElement("Directory", new XAttribute("Name", "b"), + new XElement("File", new XAttribute("Name", "C.txt"), new XElement("ResourcePath", "A.b.C.txt")), + new XElement("File", new XAttribute("Name", "c.txt"), new XElement("ResourcePath", "A.b.c.txt"))), + new XElement("File", new XAttribute("Name", "d"), new XElement("ResourcePath", "A.d")))))); + + var expectedOutput = new MemoryStream(); + var writer = XmlWriter.Create(expectedOutput, new XmlWriterSettings { Encoding = Encoding.UTF8 }); + expectedDocument.WriteTo(writer); + writer.Flush(); + expectedOutput.Seek(0, SeekOrigin.Begin); + + // Act + task.Execute(); + + // Assert + task.Output.Seek(0, SeekOrigin.Begin); + using (var expectedReader = new StreamReader(expectedOutput)) + { + using (var reader = new StreamReader(task.Output)) + { + Assert.Equal(expectedReader.ReadToEnd(), reader.ReadToEnd()); + } + } + } + + private EmbeddedItem CreateEmbeddedItem(string manifestPath, string assemblyName) => + new EmbeddedItem + { + ManifestFilePath = manifestPath, + AssemblyResourceName = assemblyName + }; + + + public class TestGenerateEmbeddedResourcesManifest + : GenerateEmbeddedResourcesManifest + { + public TestGenerateEmbeddedResourcesManifest() + : this(new MemoryStream()) + { + } + + public TestGenerateEmbeddedResourcesManifest(Stream output) + { + Output = output; + } + + public Stream Output { get; } + + protected override XmlWriter GetXmlWriter(XmlWriterSettings settings) + { + settings.CloseOutput = false; + return XmlWriter.Create(Output, settings); + } + } + + private ITaskItem[] CreateEmbeddedResource(params IDictionary[] files) => + files.Select(f => CreateTaskItem(f)).ToArray(); + + private ITaskItem CreateTaskItem(IDictionary metadata) + { + var result = new TaskItem(); + foreach (var kvp in metadata) + { + result.SetMetadata(kvp.Key, kvp.Value); + } + + return result; + } + + private static IDictionary + CreateMetadata( + string targetPath, + string manifestResourceName = null, + string logicalName = null) => + new Dictionary + { + ["TargetPath"] = targetPath, + ["ManifestResourceName"] = manifestResourceName ?? targetPath.Replace("/", ".").Replace("\\", "."), + ["LogicalName"] = logicalName ?? targetPath.Replace("/", ".").Replace("\\", "."), + }; + + private class ManifestComparer : IEqualityComparer + { + public static IEqualityComparer Instance { get; } = new ManifestComparer(); + + public bool Equals(Manifest x, Manifest y) + { + return x.Root.Equals(y.Root); + } + + public int GetHashCode(Manifest obj) + { + return obj.Root.GetHashCode(); + } + } + } +} diff --git a/src/FileProviders/Manifest.MSBuildTask/test/Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.Test.csproj b/src/FileProviders/Manifest.MSBuildTask/test/Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.Test.csproj new file mode 100644 index 0000000000..b06f1b2176 --- /dev/null +++ b/src/FileProviders/Manifest.MSBuildTask/test/Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.Test.csproj @@ -0,0 +1,11 @@ + + + + $(StandardTestTfms) + + + + + + + diff --git a/src/FileProviders/Manifest.MSBuildTask/test/SetExtensions.cs b/src/FileProviders/Manifest.MSBuildTask/test/SetExtensions.cs new file mode 100644 index 0000000000..6b2c83a875 --- /dev/null +++ b/src/FileProviders/Manifest.MSBuildTask/test/SetExtensions.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.Internal; + +namespace Microsoft.Extensions.FileProviders.Embedded.Manifest.Task +{ + internal static class SetExtensions + { + public static Entry AddRange(this Entry source, params Entry[] elements) + { + foreach (var element in elements) + { + source.Children.Add(element); + } + + return source; + } + } +} From 376a6c9953e9e71d893e8be8dc36a018eb8929f1 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Mon, 5 Nov 2018 13:09:10 -0800 Subject: [PATCH 2/2] Merge branch 'release/2.1' into release/2.2 \n\nCommit migrated from https://github.com/dotnet/extensions/commit/2d152804642f41aaeacc09307bcebb065131e75d --- .../src/Microsoft.Extensions.FileProviders.Embedded.csproj | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/FileProviders/Embedded/src/Microsoft.Extensions.FileProviders.Embedded.csproj b/src/FileProviders/Embedded/src/Microsoft.Extensions.FileProviders.Embedded.csproj index d7ca20b469..ec2c10b569 100644 --- a/src/FileProviders/Embedded/src/Microsoft.Extensions.FileProviders.Embedded.csproj +++ b/src/FileProviders/Embedded/src/Microsoft.Extensions.FileProviders.Embedded.csproj @@ -12,6 +12,12 @@ + + + + + +