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/Directory.Build.props b/src/FileProviders/Embedded/Directory.Build.props
new file mode 100644
index 0000000000..f25c1d90ce
--- /dev/null
+++ b/src/FileProviders/Embedded/Directory.Build.props
@@ -0,0 +1,7 @@
+
+
+
+
+ true
+
+
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..191330625a
--- /dev/null
+++ b/src/FileProviders/Embedded/src/Microsoft.Extensions.FileProviders.Embedded.csproj
@@ -0,0 +1,51 @@
+
+
+
+ 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 0000000000..49cc8ef0e1
Binary files /dev/null and b/src/FileProviders/Embedded/test/sub/dir/File3.txt differ
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..e6c42b46f1
--- /dev/null
+++ b/src/FileProviders/Manifest.MSBuildTask/src/Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.csproj
@@ -0,0 +1,26 @@
+
+
+
+ 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..fe33897603
--- /dev/null
+++ b/src/FileProviders/Manifest.MSBuildTask/test/Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.Test.csproj
@@ -0,0 +1,24 @@
+
+
+
+ $(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;
+ }
+ }
+}