File: AssemblyDependency\GenerateBindingRedirects.cs
Web Access
Project: ..\..\..\src\Tasks\Microsoft.Build.Tasks.csproj (Microsoft.Build.Tasks.Core)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
using System.IO;
using System.Xml;
 
#nullable disable
 
namespace Microsoft.Build.Tasks
{
    /// <summary>
    /// Take suggested redirects (from the ResolveAssemblyReference and GenerateOutOfBandAssemblyTables tasks)
    /// and add them to an intermediate copy of the App.config file.
    /// </summary>
    public class GenerateBindingRedirects : TaskExtension
    {
        // <param name="SuggestedRedirects">RAR suggested binding redirects.</param>
        // <param name="AppConfigFile">The source App.Config file.</param>
        // <param name="TargetName">The name of the target app config file: XXX.exe.config.</param>
        // <param name="OutputAppConfigFile">The output App.Config file.</param>
        // <returns>True if there was success.</returns>
 
        /// <summary>
        /// Sugested redirects as output from the ResolveAssemblyReference task.
        /// </summary>
        public ITaskItem[] SuggestedRedirects { get; set; }
 
        /// <summary>
        /// Path to the app.config source file.
        /// </summary>
        public ITaskItem AppConfigFile { get; set; }
 
        /// <summary>
        /// Name of the output application config file: $(TargetFileName).config
        /// </summary>
        public string TargetName { get; set; }
 
        /// <summary>
        /// Path to an intermediate file where we can write the input app.config plus the generated binding redirects.
        /// </summary>
        [Output]
        public ITaskItem OutputAppConfigFile { get; set; }
 
        /// <summary>
        /// Execute the task.
        /// </summary>
        public override bool Execute()
        {
            if (SuggestedRedirects == null || SuggestedRedirects.Length == 0)
            {
                Log.LogMessageFromResources("GenerateBindingRedirects.NoSuggestedRedirects");
                OutputAppConfigFile = null;
                return true;
            }
 
            var redirects = ParseSuggestedRedirects();
 
            var doc = LoadAppConfig(AppConfigFile);
 
            if (doc == null)
            {
                return false;
            }
 
            XElement runtimeNode = doc.Root
                                      .Nodes()
                                      .OfType<XElement>()
                                      .FirstOrDefault(e => e.Name.LocalName == "runtime");
 
            if (runtimeNode == null)
            {
                runtimeNode = new XElement("runtime");
                doc.Root.Add(runtimeNode);
            }
            else
            {
                UpdateExistingBindingRedirects(runtimeNode, redirects);
            }
 
            var ns = XNamespace.Get("urn:schemas-microsoft-com:asm.v1");
 
            var redirectNodes = from redirect in redirects
                                select new XElement(
                                    ns + "assemblyBinding",
                                    new XElement(
                                        ns + "dependentAssembly",
                                        new XElement(
                                            ns + "assemblyIdentity",
                                            new XAttribute("name", redirect.Key.Name),
                                            new XAttribute("publicKeyToken", ResolveAssemblyReference.ByteArrayToString(redirect.Key.GetPublicKeyToken())),
                                            new XAttribute("culture", String.IsNullOrEmpty(redirect.Key.CultureName) ? "neutral" : redirect.Key.CultureName)),
                                        new XElement(
                                            ns + "bindingRedirect",
                                            new XAttribute("oldVersion", "0.0.0.0-" + redirect.Value),
                                            new XAttribute("newVersion", redirect.Value))));
 
            runtimeNode.Add(redirectNodes);
 
            var writeOutput = true;
            var outputExists = FileSystems.Default.FileExists(OutputAppConfigFile.ItemSpec);
 
            if (outputExists)
            {
                try
                {
                    var outputDoc = LoadAppConfig(OutputAppConfigFile);
                    if (outputDoc.ToString() == doc.ToString())
                    {
                        writeOutput = false;
                    }
                }
                catch (System.Xml.XmlException)
                {
                    writeOutput = true;
                }
            }
 
            if (AppConfigFile != null)
            {
                AppConfigFile.CopyMetadataTo(OutputAppConfigFile);
            }
            else
            {
                OutputAppConfigFile.SetMetadata(ItemMetadataNames.targetPath, TargetName);
            }
 
            if (writeOutput)
            {
                Log.LogMessageFromResources(MessageImportance.Low, "GenerateBindingRedirects.CreatingBindingRedirectionFile", OutputAppConfigFile.ItemSpec);
                using (var stream = FileUtilities.OpenWrite(OutputAppConfigFile.ItemSpec, false))
                {
                    doc.Save(stream);
                }
            }
 
            return !Log.HasLoggedErrors;
        }
 
        /// <summary>
        /// Determins whether the name, culture, and public key token of the given assembly name "suggestedRedirect"
        /// matches the name, culture, and publicKeyToken strings.
        /// </summary>
        private static bool IsMatch(AssemblyName suggestedRedirect, string name, string culture, string publicKeyToken)
        {
            if (!String.Equals(suggestedRedirect.Name, name, StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }
 
            if (ByteArrayMatchesString(suggestedRedirect.GetPublicKeyToken(), publicKeyToken))
            {
                return false;
            }
 
            // The binding redirect will be applied if the culture is missing from the "assemblyIdentity" node.
            // So we consider it a match if the existing binding redirect doesn't have culture specified.
            var cultureString = suggestedRedirect.CultureName;
            if (String.IsNullOrEmpty(cultureString))
            {
                // We use "neutral" for "Invariant Language (Invariant Country/Region)" in assembly names.
                cultureString = "neutral";
            }
 
            if (!String.IsNullOrEmpty(culture) &&
                !String.Equals(cultureString, culture, StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }
 
            return true;
        }
 
        /// <summary>
        /// Determines whether string "s" is the hexdecimal representation of the byte array "a".
        /// </summary>
        private static bool ByteArrayMatchesString(Byte[] a, string s)
        {
            return !String.Equals(ResolveAssemblyReference.ByteArrayToString(a), s, StringComparison.OrdinalIgnoreCase);
        }
 
        /// <summary>
        /// Going through all the binding redirects in the runtime node, if anyone overlaps with a RAR suggested redirect,
        /// we update the existing redirect and output warning.
        /// </summary>
        private void UpdateExistingBindingRedirects(XElement runtimeNode, IDictionary<AssemblyName, string> redirects)
        {
            ErrorUtilities.VerifyThrow(runtimeNode != null, "This should not be called if the \"runtime\" node is missing.");
 
            var assemblyBindingNodes = runtimeNode.Nodes()
                .OfType<XElement>()
                .Where(e => e.Name.LocalName == "assemblyBinding");
 
            foreach (var assemblyBinding in assemblyBindingNodes)
            {
                // Each assemblyBinding section could have more than one dependentAssembly elements
                var dependentAssemblies = assemblyBinding.Nodes()
                    .OfType<XElement>()
                    .Where(e => e.Name.LocalName == "dependentAssembly");
 
                foreach (var dependentAssembly in dependentAssemblies)
                {
                    var assemblyIdentity = dependentAssembly
                        .Nodes()
                        .OfType<XElement>()
                        .FirstOrDefault(e => e.Name.LocalName == "assemblyIdentity");
 
                    if (assemblyIdentity == null)
                    {
                        // Due to MSDN documentation (https://msdn.microsoft.com/en-us/library/0ash1ksb(v=vs.110).aspx)
                        // assemblyIdentity is required subelement. Emitting a warning if it's not there.
                        Log.LogWarningWithCodeFromResources("GenerateBindingRedirects.MissingNode", "dependentAssembly", "assemblyBinding");
                        continue;
                    }
 
                    var bindingRedirect = dependentAssembly
                        .Nodes()
                        .OfType<XElement>()
                        .FirstOrDefault(e => e.Name.LocalName == "bindingRedirect");
 
                    if (bindingRedirect == null)
                    {
                        // Due to xsd schema and MSDN documentation bindingRedirect is not required subelement.
                        // Just skipping it without a warning.
                        continue;
                    }
 
                    var name = assemblyIdentity.Attribute("name");
                    var publicKeyToken = assemblyIdentity.Attribute("publicKeyToken");
 
                    if (name == null || publicKeyToken == null)
                    {
                        continue;
                    }
 
                    var nameValue = name.Value;
                    var publicKeyTokenValue = publicKeyToken.Value;
                    var culture = assemblyIdentity.Attribute("culture");
                    var cultureValue = culture?.Value ?? String.Empty;
 
                    var oldVersionAttribute = bindingRedirect.Attribute("oldVersion");
                    var newVersionAttribute = bindingRedirect.Attribute("newVersion");
 
                    if (oldVersionAttribute == null || newVersionAttribute == null)
                    {
                        continue;
                    }
 
                    var oldVersionRange = oldVersionAttribute.Value.Split(MSBuildConstants.HyphenChar);
                    if (oldVersionRange.Length == 0 || oldVersionRange.Length > 2)
                    {
                        continue;
                    }
 
                    var oldVerStrLow = oldVersionRange[0];
                    var oldVerStrHigh = oldVersionRange[oldVersionRange.Length == 1 ? 0 : 1];
 
                    if (!Version.TryParse(oldVerStrLow, out Version oldVersionLow))
                    {
                        Log.LogWarningWithCodeFromResources("GenerateBindingRedirects.MalformedVersionNumber", oldVerStrLow);
                        continue;
                    }
 
                    if (!Version.TryParse(oldVerStrHigh, out Version oldVersionHigh))
                    {
                        Log.LogWarningWithCodeFromResources("GenerateBindingRedirects.MalformedVersionNumber", oldVerStrHigh);
                        continue;
                    }
 
                    // We cannot do a simply dictionary lookup here because we want to allow relaxed "culture" matching:
                    // we consider it a match if the existing binding redirect doesn't specify culture in the assembly identity.
                    foreach (var entry in redirects)
                    {
                        if (IsMatch(entry.Key, nameValue, cultureValue, publicKeyTokenValue))
                        {
                            string maxVerStr = entry.Value;
                            var maxVersion = new Version(maxVerStr);
 
                            if (maxVersion >= oldVersionLow)
                            {
                                // Update the existing binding redirect to the RAR suggested one.
                                var newName = entry.Key.Name;
                                var newCulture = entry.Key.CultureName;
                                var newPublicKeyToken = entry.Key.GetPublicKeyToken();
                                var newProcessorArchitecture = entry.Key.ProcessorArchitecture;
 
                                var attributes = new List<XAttribute>(4)
                                {
                                    new XAttribute("name", newName),
                                    new XAttribute(
                                        "culture",
                                        String.IsNullOrEmpty(newCulture) ? "neutral" : newCulture),
                                    new XAttribute(
                                        "publicKeyToken",
                                        ResolveAssemblyReference.ByteArrayToString(newPublicKeyToken))
                                };
                                if (newProcessorArchitecture != 0)
                                {
                                    attributes.Add(new XAttribute("processorArchitecture", newProcessorArchitecture.ToString()));
                                }
 
                                assemblyIdentity.ReplaceAttributes(attributes);
 
                                oldVersionAttribute.Value = "0.0.0.0-" + (maxVersion >= oldVersionHigh ? maxVerStr : oldVerStrHigh);
                                newVersionAttribute.Value = maxVerStr;
                                redirects.Remove(entry.Key);
 
                                Log.LogWarningWithCodeFromResources("GenerateBindingRedirects.OverlappingBindingRedirect", entry.Key.ToString(), bindingRedirect.ToString());
                            }
 
                            break;
                        }
                    }
                }
            }
        }
 
        /// <summary>
        /// Load or create App.Config
        /// </summary>
        private XDocument LoadAppConfig(ITaskItem appConfigItem)
        {
            XDocument document;
            if (appConfigItem == null)
            {
                document = new XDocument(
                    new XDeclaration("1.0", "utf-8", "true"),
                    new XElement("configuration"));
            }
            else
            {
                var xrs = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore, CloseInput = true, IgnoreWhitespace = true };
                using (XmlReader xr = XmlReader.Create(File.OpenRead(appConfigItem.ItemSpec), xrs))
                {
                    document = XDocument.Load(xr);
                }
 
                if (document.Root == null || document.Root.Name != "configuration")
                {
                    Log.LogErrorWithCodeFromResources("GenerateBindingRedirects.MissingConfigurationNode");
                    return null;
                }
            }
 
            return document;
        }
 
        /// <summary>
        /// Parse the suggested redirects from RAR and return a dictionary containing all those suggested redirects
        /// in the form of AssemblyName-MaxVersion pairs.
        /// </summary>
        private IDictionary<AssemblyName, string> ParseSuggestedRedirects()
        {
            ErrorUtilities.VerifyThrow(SuggestedRedirects?.Length > 0, "This should not be called if there is no suggested redirect.");
 
            var map = new Dictionary<AssemblyName, string>();
            foreach (var redirect in SuggestedRedirects)
            {
                var redirectStr = redirect.ItemSpec;
 
                try
                {
                    var maxVerStr = redirect.GetMetadata("MaxVersion");
                    Log.LogMessageFromResources(MessageImportance.Low, "GenerateBindingRedirects.ProcessingSuggestedRedirect", redirectStr, maxVerStr);
 
                    var assemblyIdentity = new AssemblyName(redirectStr);
 
                    map.Add(assemblyIdentity, maxVerStr);
                }
                catch (Exception)
                {
                    Log.LogWarningWithCodeFromResources("GenerateBindingRedirects.MalformedAssemblyName", redirectStr);
                    continue;
                }
            }
 
            return map;
        }
    }
}