// 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.Reflection.Metadata; using System.Reflection.PortableExecutable; using Microsoft.CodeAnalysis; namespace Microsoft.AspNetCore.Razor.Tools { internal class DefaultExtensionDependencyChecker : ExtensionDependencyChecker { // These are treated as prefixes. So `Microsoft.CodeAnalysis.Razor` would be assumed to work. private static readonly string[] DefaultIgnoredAssemblies = new string[] { "mscorlib", "netstandard", "System", "Microsoft.CodeAnalysis", "Microsoft.AspNetCore.Razor.Language", }; private readonly ExtensionAssemblyLoader _loader; private readonly TextWriter _output; private readonly TextWriter _error; private readonly string[] _ignoredAssemblies; public DefaultExtensionDependencyChecker( ExtensionAssemblyLoader loader, TextWriter output, TextWriter error, string[] ignoredAssemblies = null) { _loader = loader; _output = output; _error = error; _ignoredAssemblies = ignoredAssemblies ?? DefaultIgnoredAssemblies; } public override bool Check(IEnumerable assmblyFilePaths) { try { return CheckCore(assmblyFilePaths); } catch (Exception ex) { _error.WriteLine("Exception performing Extension dependency check:"); _error.WriteLine(ex.ToString()); return false; } } private bool CheckCore(IEnumerable assemblyFilePaths) { var items = assemblyFilePaths.Select(a => ExtensionVerificationItem.Create(a)).ToArray(); var assemblies = new HashSet(items.Select(i => i.Identity)); for (var i = 0; i < items.Length; i++) { var item = items[i]; _output.WriteLine($"Verifying assembly at {item.FilePath}"); if (!Path.IsPathRooted(item.FilePath)) { _error.WriteLine($"The file path '{item.FilePath}' is not a rooted path. File paths must be absolute and fully-qualified."); return false; } foreach (var reference in item.References) { if (_ignoredAssemblies.Any(n => reference.Name.StartsWith(n))) { // This is on the allow list, keep going. continue; } if (assemblies.Contains(reference)) { // This was also provided as a dependency, keep going. continue; } // If we get here we can't resolve this assembly. This is an error. _error.WriteLine($"Extension assembly '{item.Identity.Name}' depends on '{reference.ToString()} which is missing."); return false; } } // Assuming we get this far, the set of assemblies we have is at least a coherent set (barring // version conflicts). Register all of the paths with the loader so they can find each other by // name. for (var i = 0; i < items.Length; i++) { _loader.AddAssemblyLocation(items[i].FilePath); } // Now try to load everything. This has the side effect of resolving all of these items // in the loader's caches. for (var i = 0; i < items.Length; i++) { var item = items[i]; item.Assembly = _loader.LoadFromPath(item.FilePath); } // Third, check that the MVIDs of the files on disk match the MVIDs of the loaded assemblies. for (var i = 0; i < items.Length; i++) { var item = items[i]; if (item.Mvid != item.Assembly.ManifestModule.ModuleVersionId) { _error.WriteLine($"Extension assembly '{item.Identity.Name}' at '{item.FilePath}' has a different ModuleVersionId than loaded assembly '{item.Assembly.FullName}'"); return false; } } return true; } private class ExtensionVerificationItem { public static ExtensionVerificationItem Create(string filePath) { using (var peReader = new PEReader(new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))) { var metadataReader = peReader.GetMetadataReader(); var identity = metadataReader.GetAssemblyIdentity(); var mvid = metadataReader.GetGuid(metadataReader.GetModuleDefinition().Mvid); var references = metadataReader.GetReferencedAssembliesOrThrow(); return new ExtensionVerificationItem(filePath, identity, mvid, references.ToArray()); } } private ExtensionVerificationItem(string filePath, AssemblyIdentity identity, Guid mvid, AssemblyIdentity[] references) { FilePath = filePath; Identity = identity; Mvid = mvid; References = references; } public string FilePath { get; } public Assembly Assembly { get; set; } public AssemblyIdentity Identity { get; } public Guid Mvid { get; } public IReadOnlyList References { get; } } } }