using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security;
using Microsoft.Extensions.Logging;
namespace Umbraco.Cms.Core.Composing;
///
/// Resolves assemblies that reference one of the specified "targetAssemblies" either directly or transitively.
///
///
/// Borrowed and modified from
/// https://github.com/dotnet/aspnetcore-tooling/blob/master/src/Razor/src/Microsoft.NET.Sdk.Razor/ReferenceResolver.cs
///
internal sealed class ReferenceResolver
{
private readonly IReadOnlyList _assemblies;
private readonly Dictionary _classifications;
private readonly ILogger _logger;
private readonly List _lookup = new();
private readonly HashSet _umbracoAssemblies;
public ReferenceResolver(IReadOnlyList targetAssemblies, IReadOnlyList entryPointAssemblies, ILogger logger)
{
_umbracoAssemblies = new HashSet(targetAssemblies, StringComparer.Ordinal);
_assemblies = entryPointAssemblies;
_logger = logger;
_classifications = new Dictionary();
foreach (Assembly item in entryPointAssemblies)
{
_lookup.Add(item);
}
}
private enum Classification
{
Unknown,
DoesNotReferenceUmbraco,
ReferencesUmbraco,
IsUmbraco,
}
///
/// Returns a list of assemblies that directly reference or transitively reference the targetAssemblies
///
///
///
/// This includes all assemblies in the same location as the entry point assemblies
///
public IEnumerable ResolveAssemblies()
{
var applicationParts = new List();
var assemblies = new HashSet(_assemblies);
// Get the unique directories of the assemblies
var assemblyLocations = GetAssemblyFolders(assemblies).ToList();
// Load in each assembly in the directory of the entry assembly to be included in the search
// for Umbraco dependencies/transitive dependencies
foreach (var dir in CollectionsMarshal.AsSpan(assemblyLocations))
{
foreach (var dll in Directory.EnumerateFiles(dir ?? string.Empty, "*.dll"))
{
AssemblyName? assemblyName = null;
try
{
assemblyName = AssemblyName.GetAssemblyName(dll);
}
catch (BadImageFormatException e)
{
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
{
_logger.LogDebug(e, "Could not load {dll} for type scanning, skipping", dll);
}
}
catch (SecurityException e)
{
_logger.LogError(e, "Could not access {dll} for type scanning due to a security problem", dll);
}
catch (Exception e)
{
_logger.LogInformation(e, "Error: could not load {dll} for type scanning", dll);
}
if (assemblyName != null)
{
// don't include if this is excluded
if (TypeFinder.KnownAssemblyExclusionFilter.Any(f =>
assemblyName.FullName.StartsWith(f, StringComparison.InvariantCultureIgnoreCase)))
{
continue;
}
// don't include this item if it's Umbraco Core
if (Constants.Composing.UmbracoCoreAssemblyNames.Any(x =>
assemblyName.FullName.StartsWith(x) || (assemblyName.Name?.EndsWith(".Views") ?? false)))
{
continue;
}
var assembly = Assembly.Load(assemblyName);
assemblies.Add(assembly);
}
}
}
foreach (Assembly item in assemblies)
{
Classification classification = Resolve(item);
if (classification == Classification.ReferencesUmbraco || classification == Classification.IsUmbraco)
{
applicationParts.Add(item);
}
}
return applicationParts;
}
private IEnumerable GetReferences(Assembly assembly)
{
foreach (AssemblyName referenceName in assembly.GetReferencedAssemblies())
{
// don't include if this is excluded
if (TypeFinder.KnownAssemblyExclusionFilter.Any(f =>
referenceName.FullName.StartsWith(f, StringComparison.InvariantCultureIgnoreCase)))
{
continue;
}
var reference = Assembly.Load(referenceName);
if (!_lookup.Contains(reference))
{
// A dependency references an item that isn't referenced by this project.
// We'll add this reference so that we can calculate the classification.
_lookup.Add(reference);
}
yield return reference;
}
}
private static IEnumerable GetAssemblyFolders(IEnumerable assemblies) =>
assemblies.Select(x => Path.GetDirectoryName(GetAssemblyLocation(x))).Distinct();
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Mvc/Mvc.Core/src/ApplicationParts/RelatedAssemblyAttribute.cs
private static string GetAssemblyLocation(Assembly assembly)
{
if (Uri.TryCreate(assembly.Location, UriKind.Absolute, out Uri? result) &&
result.IsFile && string.IsNullOrWhiteSpace(result.Fragment))
{
return result.LocalPath;
}
return assembly.Location;
}
private Classification Resolve(Assembly assembly)
{
if (_classifications.TryGetValue(assembly, out Classification classification))
{
return classification;
}
// Initialize the dictionary with a value to short-circuit recursive references.
classification = Classification.Unknown;
_classifications[assembly] = classification;
if (TypeFinder.KnownAssemblyExclusionFilter.Any(f =>
assembly.FullName?.StartsWith(f, StringComparison.InvariantCultureIgnoreCase) ?? false))
{
// if its part of the filter it doesn't reference umbraco
classification = Classification.DoesNotReferenceUmbraco;
}
else if (_umbracoAssemblies.Contains(assembly.GetName().Name!))
{
classification = Classification.IsUmbraco;
}
else
{
classification = Classification.DoesNotReferenceUmbraco;
foreach (Assembly reference in GetReferences(assembly))
{
// recurse
Classification referenceClassification = Resolve(reference);
if (referenceClassification == Classification.IsUmbraco ||
referenceClassification == Classification.ReferencesUmbraco)
{
classification = Classification.ReferencesUmbraco;
break;
}
}
}
Debug.Assert(classification != Classification.Unknown);
_classifications[assembly] = classification;
return classification;
}
}