diff --git a/Microsoft.Blazor.Server.Test/ReferencedAssemblyFileProviderTest.cs b/Microsoft.Blazor.Server.Test/ReferencedAssemblyFileProviderTest.cs index 42ca74ccee..28faf2a8ec 100644 --- a/Microsoft.Blazor.Server.Test/ReferencedAssemblyFileProviderTest.cs +++ b/Microsoft.Blazor.Server.Test/ReferencedAssemblyFileProviderTest.cs @@ -1,6 +1,7 @@ // 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.Blazor.Mono; using System.Linq; using Xunit; @@ -8,18 +9,33 @@ namespace Microsoft.Blazor.Server.Test { public class ReferencedAssemblyFileProviderTest { + [Fact] + public void RootDirContainsOnlyBinDir() + { + var provider = new ReferencedAssemblyFileProvider( + typeof (HostedInAspNet.Client.Program).Assembly, + MonoStaticFileProvider.Instance); + Assert.Collection(provider.GetDirectoryContents("/"), item => + { + Assert.Equal("/bin", item.PhysicalPath); + Assert.True(item.IsDirectory); + }); + } + [Fact] public void FindsEntrypointAssemblyAndReferencedAssemblies() { - var provider = new ReferencedAssemblyFileProvider(); - var contents = provider.GetDirectoryContents(string.Empty).OrderBy(i => i.Name).ToList(); + var provider = new ReferencedAssemblyFileProvider( + typeof(HostedInAspNet.Client.Program).Assembly, + MonoStaticFileProvider.Instance); + var contents = provider.GetDirectoryContents("/bin").OrderBy(i => i.Name).ToList(); Assert.Collection(contents, - item => { Assert.Equal("HostedInAspNet.Client.dll", item.PhysicalPath); }, - item => { Assert.Equal("mscorlib.dll", item.PhysicalPath); }, - item => { Assert.Equal("System.Console.dll", item.PhysicalPath); }, - item => { Assert.Equal("System.Core.dll", item.PhysicalPath); }, - item => { Assert.Equal("System.dll", item.PhysicalPath); }, - item => { Assert.Equal("System.Runtime.dll", item.PhysicalPath); }); + item => { Assert.Equal("/bin/HostedInAspNet.Client.dll", item.PhysicalPath); }, + item => { Assert.Equal("/bin/mscorlib.dll", item.PhysicalPath); }, + item => { Assert.Equal("/bin/System.Console.dll", item.PhysicalPath); }, + item => { Assert.Equal("/bin/System.Core.dll", item.PhysicalPath); }, + item => { Assert.Equal("/bin/System.dll", item.PhysicalPath); }, + item => { Assert.Equal("/bin/System.Runtime.dll", item.PhysicalPath); }); } } } diff --git a/src/Microsoft.Blazor.Common/FileProviders/InMemoryFileInfo.cs b/src/Microsoft.Blazor.Common/FileProviders/InMemoryFileInfo.cs index 731ea17005..b1c6f9b695 100644 --- a/src/Microsoft.Blazor.Common/FileProviders/InMemoryFileInfo.cs +++ b/src/Microsoft.Blazor.Common/FileProviders/InMemoryFileInfo.cs @@ -54,6 +54,7 @@ namespace Microsoft.Blazor.Internal.Common.FileProviders { dataStream.CopyTo(ms); _fileData = ms.ToArray(); + dataStream.Dispose(); } } } diff --git a/src/Microsoft.Blazor.Server/ReferencedAssemblyFileProvider.cs b/src/Microsoft.Blazor.Server/ReferencedAssemblyFileProvider.cs index 8f1018d61d..8ec5d26975 100644 --- a/src/Microsoft.Blazor.Server/ReferencedAssemblyFileProvider.cs +++ b/src/Microsoft.Blazor.Server/ReferencedAssemblyFileProvider.cs @@ -1,168 +1,155 @@ // 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 Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Primitives; using System.Collections.Generic; -using System.Collections; using System.Linq; using Mono.Cecil; -using Microsoft.Blazor.Mono; +using Microsoft.Blazor.Internal.Common.FileProviders; +using System.Reflection; +using System; namespace Microsoft.Blazor.Server { - internal class ReferencedAssemblyFileProvider : IFileProvider + internal class ReferencedAssemblyFileProvider : InMemoryFileProvider { - private static readonly Dictionary _frameworkDlls = ReadFrameworkDlls(); - private readonly Contents _referencedAssemblyContents = new Contents(FindReferencedAssemblies()); - private readonly Contents _emptyContents = new Contents(null); - - public IDirectoryContents GetDirectoryContents(string subpath) + public ReferencedAssemblyFileProvider(Assembly entrypointAssembly, IFileProvider clientBcl) + : base(ComputeContents(entrypointAssembly, clientBcl)) { - return subpath == string.Empty - ? _referencedAssemblyContents - : _emptyContents; } - public IFileInfo GetFileInfo(string subpath) + private static IEnumerable<(string, Stream)> ComputeContents( + Assembly entrypointAssembly, + IFileProvider clientBcl) { - throw new System.NotImplementedException(); - } - - public IChangeToken Watch(string filter) - { - throw new System.NotImplementedException(); - } - - private static IEnumerable FindReferencedAssemblies() - { - var foundAssemblies = new Dictionary(); - FindReferencedAssembliesRecursive( - AssemblyDefinition.ReadAssembly(typeof(TApp).Assembly.Location), + var foundAssemblies = new Dictionary(); + AddWithReferencesRecursive( + new ReferencedAssembly(AssemblyDefinition.ReadAssembly(entrypointAssembly.Location)), + clientBcl, foundAssemblies); - return foundAssemblies.Values; + + return foundAssemblies.Values.Select(assembly => ( + $"/bin/{assembly.Name}.dll", + (Stream)new MemoryStream(assembly.Data))); } - private static void FindReferencedAssembliesRecursive(AssemblyDefinition root, IDictionary results) + private static void AddWithReferencesRecursive( + ReferencedAssembly root, + IFileProvider clientBcl, + IDictionary results) { - results.Add(root.Name.Name, root); - - foreach (var module in root.Modules) + results.Add(root.Name, root); + + foreach (var module in root.Definition.Modules) { foreach (var referenceName in module.AssemblyReferences) { if (!results.ContainsKey(referenceName.Name)) { - var resolvedReference = FindAssembly(referenceName, module.AssemblyResolver); - - // Some of the referenced assemblies aren't included in the Mono BCL, e.g., - // Mono.Security.dll which is referenced from System.dll. These ones are not - // required at runtime, so just skip them. + var resolvedReference = ResolveReference(clientBcl, module, referenceName); if (resolvedReference != null) { - FindReferencedAssembliesRecursive(resolvedReference, results); + AddWithReferencesRecursive(resolvedReference, clientBcl, results); } } } } } - private static AssemblyDefinition FindAssembly(AssemblyNameReference name, IAssemblyResolver nativeResolver) + private static ReferencedAssembly ResolveReference(IFileProvider clientBcl, ModuleDefinition module, AssemblyNameReference referenceName) { - try + if (SearchInFileProvider(clientBcl, string.Empty, $"{referenceName.Name}.dll", out var bclFile)) { - return _frameworkDlls.TryGetValue($"{name.Name}.dll", out var fileInfo) - ? AssemblyDefinition.ReadAssembly(fileInfo.CreateReadStream()) - : AllowNativeDllResolution(name) ? nativeResolver.Resolve(name) : null; + // Where possible, we resolve references to client BCL assemblies + return new ReferencedAssembly(bclFile); } - catch (AssemblyResolutionException) + else { - return null; - } - } - - private static bool AllowNativeDllResolution(AssemblyNameReference name) - { - // System.* assemblies must only be resolved from the browser-reachable FrameworkFiles. - // It's no use resolving them using the native resolver, because those files wouldn't - // be accessible at runtime anyway. - return !name.Name.StartsWith("System.", StringComparison.Ordinal); - } - - private static Dictionary ReadFrameworkDlls() - { - // TODO: Stop leaking knowledge of the Microsoft.Blazor.Mono file provider internal - // structure into this unrelated class. Currently it's needed because that file provider - // doesn't support proper directory hierarchies and therefore keeps all files in the - // top-level directory, putting '$' into filenames in place of directories. - // To fix this, make MonoStaticFileProvider expose a regular directory structure, and - // then change this method to walk it recursively. - - return MonoStaticFileProvider.Instance - .GetDirectoryContents(string.Empty) - .Where(file => file.Name.EndsWith(".dll", StringComparison.Ordinal)) - .ToDictionary(MonoEmbeddedResourceToFilename, file => file); - - string MonoEmbeddedResourceToFilename(IFileInfo fileInfo) - { - var name = fileInfo.Name; - var lastDirSeparatorPos = name.LastIndexOf('$'); - return name.Substring(lastDirSeparatorPos + 1); - } - } - - private class Contents : IDirectoryContents - { - private readonly bool _exists; - private readonly IReadOnlyDictionary _items; - - public Contents(IEnumerable assemblies) - { - _exists = assemblies != null; - _items = (assemblies ?? Enumerable.Empty()) - .Select(assembly => new AssemblyFileInfo(assembly)) - .ToDictionary(item => item.Name, item => (IFileInfo)item); - } - - public bool Exists => _exists; - - public IEnumerator GetEnumerator() => _items.Values.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => _items.Values.GetEnumerator(); - } - - private class AssemblyFileInfo : IFileInfo - { - private readonly byte[] _data; - - public bool Exists => true; - - public long Length => _data.Length; - - public string PhysicalPath => Name; - - public string Name { get; } - - public DateTimeOffset LastModified => default(DateTimeOffset); - - public bool IsDirectory => false; - - public Stream CreateReadStream() - { - return new MemoryStream(_data); - } - - public AssemblyFileInfo(AssemblyDefinition assembly) - { - Name = $"{assembly.Name.Name}.dll"; - - using (var ms = new MemoryStream()) + try { - assembly.Write(ms); - _data = ms.GetBuffer(); + // If it's not a client BCL assembly, maybe we can resolve it natively + // (e.g., if it's in the app's bin directory, or a NuGet package) + var nativelyResolved = module.AssemblyResolver.Resolve(referenceName); + return AllowServingAssembly(nativelyResolved) + ? new ReferencedAssembly(nativelyResolved) + : null; } + catch (AssemblyResolutionException) + { + // Some of the referenced assemblies aren't included in the Mono BCL, e.g., + // Mono.Security.dll which is referenced from System.dll. These ones are not + // required at runtime, so just skip them. + return null; + } + } + } + + private static bool AllowServingAssembly(AssemblyDefinition nativelyResolvedAssembly) + { + // When we use the native assembly resolver, it might return something from a NuGet + // packages folder which we do want to serve, or it might return something from the + // core .NET BCL which we *don't* want to serve (because the core BCL assemblies + // should come from the Mono WASM distribution only). Currently there isn't a good + // way to differentiate these cases, so as a temporary heuristic, assume anything + // named System.* shouldn't be resolved natively. + return !nativelyResolvedAssembly.MainModule.Name.StartsWith( + "System.", + StringComparison.Ordinal); + } + + private static bool SearchInFileProvider(IFileProvider fileProvider, string searchRootDirNoTrailingSlash, string name, out IFileInfo file) + { + var possibleFullPath = $"{searchRootDirNoTrailingSlash}/{name}"; + var possibleResult = fileProvider.GetFileInfo(possibleFullPath); + if (possibleResult.Exists) + { + file = possibleResult; + return true; + } + + var childDirs = fileProvider.GetDirectoryContents(searchRootDirNoTrailingSlash) + .Where(item => item.IsDirectory); + foreach (var childDir in childDirs) + { + if (SearchInFileProvider(fileProvider, childDir.PhysicalPath, name, out file)) + { + return true; + } + } + + file = null; + return false; + } + + private class ReferencedAssembly + { + public string Name { get; } + public byte[] Data { get; } + public AssemblyDefinition Definition { get; } + + public ReferencedAssembly(AssemblyDefinition definition) + { + Name = definition.Name.Name; + Data = File.ReadAllBytes(definition.MainModule.FileName); + Definition = definition; + } + + public ReferencedAssembly(IFileInfo fileInfo) + { + using (var ms = new MemoryStream()) + using (var readStream = fileInfo.CreateReadStream()) + { + readStream.CopyTo(ms); + Data = ms.ToArray(); + } + + using (var readStream = new MemoryStream(Data)) + { + Definition = AssemblyDefinition.ReadAssembly(readStream); + } + + Name = Definition.Name.Name; } } }