File: ContentModel\ManagedCodeConventions.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Packaging\NuGet.Packaging.csproj (NuGet.Packaging)
// 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.Collections.ObjectModel;
using NuGet.ContentModel;
using NuGet.Frameworks;
using NuGet.Packaging.Core;
using NuGet.RuntimeModel;

namespace NuGet.Client
{
    /// <summary>
    /// Defines all the package conventions used by Managed Code packages
    /// </summary>
    public class ManagedCodeConventions
    {
        private static readonly ContentPropertyDefinition LocaleProperty = new ContentPropertyDefinition(PropertyNames.Locale,
            parser: Locale_Parser);

        private static readonly ContentPropertyDefinition AnyProperty = new ContentPropertyDefinition(
            PropertyNames.AnyValue,
            parser: IdentityParser); // Identity parser, all strings are valid for any
        private static readonly ContentPropertyDefinition AssemblyProperty = new ContentPropertyDefinition(PropertyNames.ManagedAssembly,
            parser: AllowEmptyFolderParser,
            fileExtensions: new[] { ".dll", ".winmd", ".exe" });
        private static readonly ContentPropertyDefinition MSBuildProperty = new ContentPropertyDefinition(PropertyNames.MSBuild,
            parser: AllowEmptyFolderParser,
            fileExtensions: new[] { ".targets", ".props" });
        private static readonly ContentPropertyDefinition SatelliteAssemblyProperty = new ContentPropertyDefinition(PropertyNames.SatelliteAssembly,
            parser: AllowEmptyFolderParser,
            fileExtensions: new[] { ".resources.dll" });

        private static readonly ContentPropertyDefinition CodeLanguageProperty = new ContentPropertyDefinition(
            PropertyNames.CodeLanguage,
            parser: CodeLanguage_Parser);

        private static readonly Dictionary<string, object> NetTFMTable = new Dictionary<string, object>
        {
            { "tfm", new NuGetFramework(FrameworkConstants.FrameworkIdentifiers.Net, FrameworkConstants.EmptyVersion) },
            { "tfm_raw", "net0" }
        };

        private static readonly Dictionary<string, object> DefaultTfmAny = new Dictionary<string, object>
        {
            { PropertyNames.TargetFrameworkMoniker, AnyFramework.Instance },
            { PropertyNames.TargetFrameworkMoniker + "_raw", "any" }
        };

        private static readonly PatternTable DotnetAnyTable = new PatternTable(new[]
        {
            new PatternTableEntry(
                PropertyNames.TargetFrameworkMoniker,
                "any",
                FrameworkConstants.CommonFrameworks.DotNet)
        });

        private static readonly PatternTable AnyTable = new PatternTable(new[]
        {
            new PatternTableEntry(
                PropertyNames.TargetFrameworkMoniker,
                "any",
                AnyFramework.Instance )
        });

        private static readonly FrameworkReducer FrameworkReducer = new();

        private RuntimeGraph? _runtimeGraph;

        private Dictionary<ReadOnlyMemory<char>, NuGetFramework> _frameworkCache = new(ReadOnlyMemoryCharComparerOrdinal.Instance);

        public ManagedCodeCriteria Criteria { get; }
        public IReadOnlyDictionary<string, ContentPropertyDefinition> Properties { get; }
        public ManagedCodePatterns Patterns { get; }

        public ManagedCodeConventions(RuntimeGraph? runtimeGraph)
        {
            _runtimeGraph = runtimeGraph;

            var props = new Dictionary<string, ContentPropertyDefinition>();
            props[AnyProperty.Name] = AnyProperty;
            props[AssemblyProperty.Name] = AssemblyProperty;
            props[LocaleProperty.Name] = LocaleProperty;
            props[MSBuildProperty.Name] = MSBuildProperty;
            props[SatelliteAssemblyProperty.Name] = SatelliteAssemblyProperty;
            props[CodeLanguageProperty.Name] = CodeLanguageProperty;

            props[PropertyNames.RuntimeIdentifier] = new ContentPropertyDefinition(
                PropertyNames.RuntimeIdentifier,
                parser: IdentityParser, // Identity parser, all strings are valid runtime ids
                compatibilityTest: RuntimeIdentifier_CompatibilityTest);

            props[PropertyNames.TargetFrameworkMoniker] = new ContentPropertyDefinition(
                PropertyNames.TargetFrameworkMoniker,
                parser: TargetFrameworkName_Parser,
                compatibilityTest: TargetFrameworkName_CompatibilityTest,
                compareTest: TargetFrameworkName_NearestCompareTest);

            Properties = new ReadOnlyDictionary<string, ContentPropertyDefinition>(props);

            Criteria = new ManagedCodeCriteria(this);
            Patterns = new ManagedCodePatterns(this);
        }

        private bool RuntimeIdentifier_CompatibilityTest(object? criteria, object? available)
        {
            if (_runtimeGraph == null)
            {
                return Equals(criteria, available);
            }
            else
            {
                var criteriaRid = criteria as string;
                var availableRid = available as string;

                if (criteriaRid != null
                    && availableRid != null)
                {
                    return _runtimeGraph.AreCompatible(criteriaRid, availableRid);
                }
                return false;
            }
        }

        /// <summary>
        /// If matchOnly is true, then an empty string may be returned as a performance optimization.
        /// If matchOnly is false, the parsed result will be returned.
        /// </summary>
        private static object? CodeLanguage_Parser(ReadOnlyMemory<char> name, PatternTable? table, bool matchOnly)
        {
            if (table != null)
            {
                object? val;
                if (table.TryLookup(PropertyNames.CodeLanguage, name, out val))
                {
                    return val;
                }
            }

            // Code language values must be alpha numeric.
            // PERF: use foreach to avoid CharEnumerator allocation
            foreach (char c in name.Span)
            {
                if (!char.IsLetterOrDigit(c))
                {
                    return null;
                }
            }

            if (matchOnly)
            {
                return string.Empty;
            }

            return name.ToString();
        }

        /// <summary>
        /// If matchOnly is true, then an empty string may be returned as a performance optimization.
        /// If matchOnly is false, the parsed result will be returned.
        /// </summary>
        internal static object? Locale_Parser(ReadOnlyMemory<char> name, PatternTable? table, bool matchOnly)
        {
            if (table != null)
            {
                object? val;
                if (table.TryLookup(PropertyNames.Locale, name, out val))
                {
                    return val;
                }
            }

            // We use a heuristic here for common locale codes. Locale codes are often
            // * two characters for the language: en, es, fr, de
            // * three characters for the language: agq
            if (name.Length == 2 || name.Length == 3)
            {
                if (matchOnly)
                {
                    return string.Empty;
                }
                return name.ToString();
            }

            // * a language portion that is two or three characters followed by a '-' and a country code
            else if (name.Length >= 4 && name.Span[2] == '-') // e.g. en-US
            {
                if (matchOnly)
                {
                    return string.Empty;
                }
                return name.ToString();
            }
            else if (name.Length >= 5 && name.Span[3] == '-') // e.g agq-CM
            {
                if (matchOnly)
                {
                    return string.Empty;
                }
                return name.ToString();
            }

            // there are other variations, but this heuristic doesn't cover them all. A future-proof implementation would make
            // use of the .NET CultureInfo APIs to compare the locale against the underlying system ICU database. This would
            // be correct, but potentially more expensive because the CultureInfo APIs are lazily-loaded and throw if an
            // invalid/unknown locale is used.

            return null;
        }

        private object TargetFrameworkName_Parser(
            ReadOnlyMemory<char> name,
            PatternTable? table,
            bool matchOnly)
        {
            // Check for replacements
            if (table != null)
            {
                if (table.TryLookup(PropertyNames.TargetFrameworkMoniker, name, out var obj))
                {
                    return obj;
                }
            }

            // Check the cache for an exact match
            if (!name.IsEmpty)
            {
                if (!_frameworkCache.TryGetValue(name, out NuGetFramework? cachedResult))
                {
                    // Parse and add the framework to the cache
                    cachedResult = TargetFrameworkName_ParserCore(name.ToString());
                    _frameworkCache.Add(name, cachedResult);
                }

                return cachedResult;
            }

            // Let the framework parser handle null/empty and create the error message.
            return TargetFrameworkName_ParserCore(name.ToString());
        }

        private static NuGetFramework TargetFrameworkName_ParserCore(string name)
        {
            var result = NuGetFramework.ParseFolder(name);

            if (!result.IsUnsupported)
            {
                return result;
            }

            // Everything should be in the folder format, but fallback to
            // full parsing for legacy support.
            result = NuGetFramework.ParseFrameworkName(name, DefaultFrameworkNameProvider.Instance);

            if (!result.IsUnsupported)
            {
                return result;
            }

            // For unknown frameworks return the name as is.
            return new NuGetFramework(name, FrameworkConstants.EmptyVersion);
        }

        /// <summary>
        /// Identity parser, returns the input string as is.
        /// If matchOnly is true, then an empty string is returned as a performance optimization.
        /// If matchOnly is false, the string will be actualized.
        /// </summary>
        private static object IdentityParser(ReadOnlyMemory<char> s, PatternTable? _, bool matchOnly)
        {
            if (matchOnly)
            {
                return string.Empty;
            }
            return s.ToString();
        }


        /// <summary>
        /// If matchOnly is true, then an empty string is returned as a performance optimization.
        /// If matchOnly is false, the parsed result will be returned.
        /// </summary>
        private static object? AllowEmptyFolderParser(ReadOnlyMemory<char> s, PatternTable? _, bool matchOnly)
        {
            // Accept "_._" as a pseudo-assembly
            if (MemoryExtensions.Equals(PackagingCoreConstants.EmptyFolder.AsSpan(), s.Span, StringComparison.Ordinal))
            {
                if (matchOnly)
                {
                    return string.Empty;
                }
                return PackagingCoreConstants.EmptyFolder;
            }

            return null;
        }

        private static bool TargetFrameworkName_CompatibilityTest(object? criteria, object? available)
        {
            var criteriaFrameworkName = criteria as NuGetFramework;
            var availableFrameworkName = available as NuGetFramework;

            if (criteriaFrameworkName != null
                && availableFrameworkName != null)
            {
                // We only consider 'any' matches when the criteria explicitly asks for them
                if (criteriaFrameworkName.IsAny
                    && availableFrameworkName.IsAny)
                {
                    return true;
                }
                else if (Object.Equals(AnyFramework.AnyFramework, availableFrameworkName))
                {
                    // If the convention does not contain a TxM it will use AnyFramework, this is
                    // always compatible with other frameworks.
                    return true;
                }
                else if (criteriaFrameworkName.IsAny
                         || availableFrameworkName.IsAny)
                {
                    // Otherwise, ignore 'any' framework values
                    return false;
                }

                return NuGetFrameworkUtility.IsCompatibleWithFallbackCheck(criteriaFrameworkName, availableFrameworkName);
            }

            return false;
        }

        private static int TargetFrameworkName_NearestCompareTest(object? projectFramework, object? criteria, object? available)
        {
            var projectFrameworkName = projectFramework as NuGetFramework;
            var criteriaFrameworkName = criteria as NuGetFramework;
            var availableFrameworkName = available as NuGetFramework;

            if (criteriaFrameworkName != null
                && availableFrameworkName != null
                && projectFrameworkName != null)
            {
                // If the frameworks are the same this can be skipped
                if (!criteriaFrameworkName.Equals(availableFrameworkName))
                {
                    var frameworks = new NuGetFramework[] { criteriaFrameworkName, availableFrameworkName };

                    // Find the nearest compatible framework to the project framework.
                    var nearest = FrameworkReducer.GetNearest(projectFrameworkName, frameworks);

                    if (criteriaFrameworkName.Equals(nearest))
                    {
                        return -1;
                    }

                    if (availableFrameworkName.Equals(nearest))
                    {
                        return 1;
                    }
                }
            }

            return 0;
        }

        public class ManagedCodeCriteria
        {
            private ManagedCodeConventions _conventions;

            internal ManagedCodeCriteria(ManagedCodeConventions conventions)
            {
                _conventions = conventions;
            }

            public SelectionCriteria ForFrameworkAndRuntime(NuGetFramework framework, string? runtimeIdentifier)
            {
                if (framework is FallbackFramework)
                {
                    // Fallback frameworks are not handled by content model
                    throw new NotSupportedException("FallbackFramework is not supported.");
                }

                // Both criteria must specify a RID

                var builder = new SelectionCriteriaBuilder(_conventions.Properties);
                if (!string.IsNullOrEmpty(runtimeIdentifier))
                {
                    builder = builder
                        // Take runtime-specific matches first!
                        .Add[PropertyNames.TargetFrameworkMoniker, framework][PropertyNames.RuntimeIdentifier, runtimeIdentifier];
                }

                // Then try runtime-agnostic
                builder = builder
                    .Add[PropertyNames.TargetFrameworkMoniker, framework][PropertyNames.RuntimeIdentifier, value: null];

                return builder.Criteria;
            }

            public SelectionCriteria ForFramework(NuGetFramework framework)
            {
                return ForFrameworkAndRuntime(framework, runtimeIdentifier: null);
            }

            public SelectionCriteria ForRuntime(string runtimeIdentifier)
            {
                var builder = new SelectionCriteriaBuilder(_conventions.Properties);
                builder = builder
                    .Add[PropertyNames.RuntimeIdentifier, runtimeIdentifier];
                return builder.Criteria;
            }
        }

        public class ManagedCodePatterns
        {
            /// <summary>
            /// Pattern used to locate all files targetted at a specific runtime and/or framework
            /// </summary>
            public PatternSet AnyTargettedFile { get; }

            /// <summary>
            /// Pattern used to locate all files designed for loading as managed code assemblies at run-time
            /// </summary>
            public PatternSet RuntimeAssemblies { get; }

            /// <summary>
            /// Pattern used to locate ref assemblies for compile.
            /// </summary>
            public PatternSet CompileRefAssemblies { get; }

            /// <summary>
            /// Pattern used to locate lib assemblies for compile.
            /// </summary>
            public PatternSet CompileLibAssemblies { get; }

            /// <summary>
            /// Pattern used to locate all files designed for loading as native code libraries at run-time
            /// </summary>
            public PatternSet NativeLibraries { get; }

            /// <summary>
            /// Pattern used to locate all files designed for loading as managed code resource assemblies at run-time
            /// </summary>
            public PatternSet ResourceAssemblies { get; }

            /// <summary>
            /// Pattern used to identify MSBuild targets and props files
            /// </summary>
            public PatternSet MSBuildFiles { get; }

            /// <summary>
            /// Pattern used to identify MSBuild global targets and props files
            /// </summary>
            public PatternSet MSBuildMultiTargetingFiles { get; }

            /// <summary>
            /// Pattern used to identify content files
            /// </summary>
            public PatternSet ContentFiles { get; }

            /// <summary>
            /// Pattern used to identify Tools assets for global tools
            /// </summary>
            public PatternSet ToolsAssemblies { get; }

            /// <summary>
            /// Pattern used to locate embed interop types assemblies
            /// </summary>
            public PatternSet EmbedAssemblies { get; }

            /// <summary>
            /// Pattern used to identify MSBuild transitive targets and props files
            /// </summary>
            public PatternSet MSBuildTransitiveFiles { get; }

            internal ManagedCodePatterns(ManagedCodeConventions conventions)
            {
                AnyTargettedFile = new PatternSet(
                    conventions.Properties,
                    groupPatterns: new PatternDefinition[]
                    {
                        new PatternDefinition("{any}/{tfm}/{any?}", table: DotnetAnyTable),
                        new PatternDefinition("runtimes/{rid}/{any}/{tfm}/{any?}", table: DotnetAnyTable),
                    },
                    pathPatterns: new PatternDefinition[]
                    {
                        new PatternDefinition("{any}/{tfm}/{any?}", table: DotnetAnyTable),
                        new PatternDefinition("runtimes/{rid}/{any}/{tfm}/{any?}", table: DotnetAnyTable),
                    });

                RuntimeAssemblies = new PatternSet(
                    conventions.Properties,
                    groupPatterns: new PatternDefinition[]
                    {
                        new PatternDefinition("runtimes/{rid}/lib/{tfm}/{any?}", table: DotnetAnyTable),
                        new PatternDefinition("lib/{tfm}/{any?}", table: DotnetAnyTable),
                        new PatternDefinition("lib/{assembly?}", table: DotnetAnyTable, defaults: NetTFMTable)
                    },
                    pathPatterns: new PatternDefinition[]
                    {
                        new PatternDefinition("runtimes/{rid}/lib/{tfm}/{assembly}", table: DotnetAnyTable),
                        new PatternDefinition("lib/{tfm}/{assembly}", table: DotnetAnyTable),
                        new PatternDefinition("lib/{assembly}", table: DotnetAnyTable, defaults: NetTFMTable)
                    });

                CompileRefAssemblies = new PatternSet(
                    conventions.Properties,
                    groupPatterns: new PatternDefinition[]
                        {
                            new PatternDefinition("ref/{tfm}/{any?}", table: DotnetAnyTable),
                        },
                    pathPatterns: new PatternDefinition[]
                        {
                            new PatternDefinition("ref/{tfm}/{assembly}", table: DotnetAnyTable),
                        });

                CompileLibAssemblies = new PatternSet(
                    conventions.Properties,
                    groupPatterns: new PatternDefinition[]
                        {
                            new PatternDefinition("lib/{tfm}/{any?}", table: DotnetAnyTable),
                            new PatternDefinition("lib/{assembly?}", table: DotnetAnyTable, defaults: NetTFMTable)
                        },
                    pathPatterns: new PatternDefinition[]
                        {
                            new PatternDefinition("lib/{tfm}/{assembly}", table: DotnetAnyTable),
                            new PatternDefinition("lib/{assembly}", table: DotnetAnyTable, defaults: NetTFMTable)
                        });

                NativeLibraries = new PatternSet(
                    conventions.Properties,
                    groupPatterns: new PatternDefinition[]
                        {
                            new PatternDefinition("runtimes/{rid}/nativeassets/{tfm}/{any?}", table: DotnetAnyTable),
                            new PatternDefinition("runtimes/{rid}/native/{any?}", table: null, defaults: DefaultTfmAny)
                        },
                    pathPatterns: new PatternDefinition[]
                    {
                        new PatternDefinition("runtimes/{rid}/nativeassets/{tfm}/{any}", table: DotnetAnyTable),
                        new PatternDefinition("runtimes/{rid}/native/{any}", table: null, defaults: DefaultTfmAny)
                    });

                ResourceAssemblies = new PatternSet(
                    conventions.Properties,
                    groupPatterns: new PatternDefinition[]
                    {
                        new PatternDefinition("runtimes/{rid}/lib/{tfm}/{locale?}/{any?}", table: DotnetAnyTable),
                        new PatternDefinition("lib/{tfm}/{locale?}/{any?}", table: DotnetAnyTable),
                    },
                    pathPatterns: new PatternDefinition[]
                    {
                        new PatternDefinition("runtimes/{rid}/lib/{tfm}/{locale}/{satelliteAssembly}", table: DotnetAnyTable),
                        new PatternDefinition("lib/{tfm}/{locale}/{satelliteAssembly}", table: DotnetAnyTable),
                    });

                MSBuildFiles = new PatternSet(
                    conventions.Properties,
                    groupPatterns: new PatternDefinition[]
                    {
                        new PatternDefinition("build/{tfm}/{msbuild?}", table: DotnetAnyTable),
                        new PatternDefinition("build/{msbuild?}", table: null, defaults: DefaultTfmAny)
                    },
                    pathPatterns: new PatternDefinition[]
                    {
                        new PatternDefinition("build/{tfm}/{msbuild}", table: DotnetAnyTable),
                        new PatternDefinition("build/{msbuild}", table: null, defaults: DefaultTfmAny)
                    });

                MSBuildMultiTargetingFiles = new PatternSet(
                    conventions.Properties,
                    groupPatterns: new PatternDefinition[]
                    {
                        new PatternDefinition("buildMultiTargeting/{msbuild?}", table: null, defaults: DefaultTfmAny),

                        // deprecated
                        new PatternDefinition("buildCrossTargeting/{msbuild?}", table: null, defaults: DefaultTfmAny)
                    },
                    pathPatterns: new PatternDefinition[]
                    {
                        new PatternDefinition("buildMultiTargeting/{msbuild}", table: null, defaults: DefaultTfmAny),

                        // deprecated
                        new PatternDefinition("buildCrossTargeting/{msbuild}", table: null, defaults: DefaultTfmAny)
                    });

                ContentFiles = new PatternSet(
                    conventions.Properties,
                    groupPatterns: new PatternDefinition[]
                    {
                        new PatternDefinition("contentFiles/{codeLanguage}/{tfm}/{any?}"),
                    },
                    pathPatterns: new PatternDefinition[]
                    {
                        new PatternDefinition("contentFiles/{codeLanguage}/{tfm}/{any?}"),
                    });

                ToolsAssemblies = new PatternSet(
                    conventions.Properties,
                    groupPatterns: new PatternDefinition[]
                        {
                            new PatternDefinition("tools/{tfm}/{rid}/{any?}", table: AnyTable),
                        },
                    pathPatterns: new PatternDefinition[]
                        {
                            new PatternDefinition("tools/{tfm}/{rid}/{any?}", table: AnyTable),
                    });

                EmbedAssemblies = new PatternSet(
                    conventions.Properties,
                    groupPatterns: new PatternDefinition[]
                        {
                            new PatternDefinition("embed/{tfm}/{any?}", table: DotnetAnyTable),
                        },
                    pathPatterns: new PatternDefinition[]
                        {
                            new PatternDefinition("embed/{tfm}/{assembly}", table: DotnetAnyTable),
                        });

                MSBuildTransitiveFiles = new PatternSet(
                    conventions.Properties,
                    groupPatterns: new PatternDefinition[]
                    {
                        new PatternDefinition("buildTransitive/{tfm}/{msbuild?}", table: DotnetAnyTable),
                        new PatternDefinition("buildTransitive/{msbuild?}", table: null, defaults: DefaultTfmAny)
                    },
                    pathPatterns: new PatternDefinition[]
                    {
                        new PatternDefinition("buildTransitive/{tfm}/{msbuild}", table: DotnetAnyTable),
                        new PatternDefinition("buildTransitive/{msbuild}", table: null, defaults: DefaultTfmAny)
                    });
            }
        }

        public static class PropertyNames
        {
            public static readonly string TargetFrameworkMoniker = "tfm";
            public static readonly string RuntimeIdentifier = "rid";
            public static readonly string AnyValue = "any";
            public static readonly string ManagedAssembly = "assembly";
            public static readonly string Locale = "locale";
            public static readonly string MSBuild = "msbuild";
            public static readonly string SatelliteAssembly = "satelliteAssembly";
            public static readonly string CodeLanguage = "codeLanguage";
        }
    }
}