File: ContentModel\ContentItemCollection.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.Concurrent;
using System.Collections.Generic;
using System.Linq;
using NuGet.Client;
using NuGet.Shared;

namespace NuGet.ContentModel
{
    public class ContentItemCollection
    {
        private static readonly ReadOnlyMemory<char> Dll = ".dll".AsMemory();
        private static readonly ReadOnlyMemory<char> Exe = ".exe".AsMemory();
        private static readonly ReadOnlyMemory<char> Winmd = ".winmd".AsMemory();

        private static readonly SimplePool<List<Asset>> ListAssetPool = new(() => new List<Asset>());
        private static readonly SimplePool<Dictionary<ContentItem, List<Asset>>> GroupAssetsPool = new(() => new(GroupComparer.DefaultComparer));

        private List<Asset>? _assets;
        private ConcurrentDictionary<ReadOnlyMemory<char>, string?>? _assemblyRelatedExtensions;
        /// <summary>
        /// True if lib/contract exists
        /// </summary>
        public bool HasContract { get; private set; }

        public void Load(IEnumerable<string> paths)
        {
            if (paths == null)
            {
                throw new ArgumentNullException(nameof(paths));
            }
            // Cache for assembly and it's related file extensions.
            _assemblyRelatedExtensions = new();

            // Read already loaded assets
            _assets = new List<Asset>();

            foreach (var path in paths.NoAllocEnumerate())
            {
                // Skip files in the root of the directory
                if (IsValidAsset(path))
                {
                    _assets.Add(new Asset()
                    {
                        Path = path
                    });

                    if (path.StartsWith("lib/contract", StringComparison.Ordinal))
                    {
                        HasContract = true;

                        _assets.Add(new Asset
                        {
                            Path = "ref/any" + path.Substring("lib/contract".Length)
                        });
                    }
                }
            }
        }

        public IEnumerable<ContentItem> FindItems(PatternSet definition)
        {
            if (definition == null)
            {
                throw new ArgumentNullException(nameof(definition));
            }
            if (_assets != null)
            {
                return FindItemsImplementation(definition, _assets);
            }
            return Enumerable.Empty<ContentItem>();
        }

        /// <summary>
        /// Populate the provided list with ContentItemGroups based on a provided pattern set.
        /// </summary>
        /// <param name="definition">The pattern set to match</param>
        /// <param name="contentItemGroupList">The list that will be mutated and populated with the item groups</param>
        public void PopulateItemGroups(PatternSet definition, IList<ContentItemGroup> contentItemGroupList)
        {
            if (definition == null)
            {
                throw new ArgumentNullException(nameof(definition));
            }
            if (contentItemGroupList == null)
            {
                throw new ArgumentNullException(nameof(contentItemGroupList));
            }
            if (_assets != null && _assets.Count > 0)
            {
                var groupPatterns = definition.GroupExpressions;

                Dictionary<ContentItem, List<Asset>>? groupAssets = null;
                foreach (var asset in _assets)
                {
                    foreach (var groupPattern in groupPatterns)
                    {
                        var item = groupPattern.Match(asset.Path, definition.PropertyDefinitions);
                        if (item != null)
                        {
                            groupAssets ??= GroupAssetsPool.Allocate();
                            if (!groupAssets.TryGetValue(item, out var assets))
                            {
                                assets = ListAssetPool.Allocate();
                                groupAssets[item] = assets;
                            }

                            assets.Add(asset);
                        }
                    }
                }

                if (groupAssets != null)
                {
                    // Pre-size the list to avoid repeated array allocations during Add operations.
                    // Callers typically pass a List but check just to be safe.
                    if (contentItemGroupList is List<ContentItemGroup> list)
                    {
                        list.Capacity = Math.Max(list.Capacity, contentItemGroupList.Count + groupAssets.Count);
                    }

                    foreach (var (item, assets) in groupAssets)
                    {
                        contentItemGroupList.Add(new ContentItemGroup(
                            properties: item.Properties,
                            items: FindItemsImplementation(definition, assets)));

                        assets.Clear();
                        ListAssetPool.Free(assets);
                    }

                    groupAssets.Clear();
                    GroupAssetsPool.Free(groupAssets);
                }
            }
        }

        public ContentItemGroup? FindBestItemGroup(SelectionCriteria criteria, params PatternSet[] definitions)
        {
            if (criteria is null)
            {
                throw new ArgumentNullException(nameof(criteria));
            }
            if (definitions.Length == 0)
            {
                return null;
            }

            List<ContentItemGroup> itemGroups = new List<ContentItemGroup>();
            foreach (var definition in definitions)
            {
                itemGroups.Clear();
                PopulateItemGroups(definition, itemGroups);
                foreach (var criteriaEntry in criteria.Entries.NoAllocEnumerate())
                {
                    ContentItemGroup? bestGroup = null;
                    var bestAmbiguity = false;

                    foreach (var itemGroup in itemGroups)
                    {
                        var groupIsValid = true;
                        foreach (var criteriaProperty in criteriaEntry.Properties.NoAllocEnumerate())
                        {
                            if (criteriaProperty.Value == null)
                            {
                                if (itemGroup.Properties.ContainsKey(criteriaProperty.Key))
                                {
                                    groupIsValid = false;
                                    break;
                                }
                            }
                            else
                            {
                                object? itemProperty;
                                if (!itemGroup.Properties.TryGetValue(criteriaProperty.Key, out itemProperty))
                                {
                                    groupIsValid = false;
                                    break;
                                }
                                ContentPropertyDefinition? propertyDefinition;
                                if (!definition.PropertyDefinitions.TryGetValue(criteriaProperty.Key, out propertyDefinition))
                                {
                                    groupIsValid = false;
                                    break;
                                }
                                if (!propertyDefinition.IsCriteriaSatisfied(criteriaProperty.Value, itemProperty))
                                {
                                    groupIsValid = false;
                                    break;
                                }
                            }
                        }
                        if (groupIsValid)
                        {
                            if (bestGroup == null)
                            {
                                bestGroup = itemGroup;
                            }
                            else
                            {
                                var groupComparison = 0;
                                foreach (var criteriaProperty in criteriaEntry.Properties.NoAllocEnumerate())
                                {
                                    if (criteriaProperty.Value == null)
                                    {
                                        continue;
                                    }
                                    else
                                    {
                                        var bestGroupValue = bestGroup.Properties[criteriaProperty.Key];
                                        var itemGroupValue = itemGroup.Properties[criteriaProperty.Key];
                                        var propertyDefinition = definition.PropertyDefinitions[criteriaProperty.Key];
                                        groupComparison = propertyDefinition.Compare(criteriaProperty.Value, bestGroupValue, itemGroupValue);
                                        if (groupComparison != 0)
                                        {
                                            break;
                                        }
                                    }
                                }
                                if (groupComparison > 0)
                                {
                                    bestGroup = itemGroup;
                                    bestAmbiguity = false;
                                }
                                else if (groupComparison == 0)
                                {
                                    bestAmbiguity = true;
                                }
                            }
                        }
                    }
                    if (bestGroup != null)
                    {
                        if (bestAmbiguity)
                        {
                            // var x = 5;
                        }
                        return bestGroup;
                    }
                }
            }
            return null;
        }

        private List<ContentItem> FindItemsImplementation(PatternSet definition, IEnumerable<Asset> assets)
        {
            var pathPatterns = definition.PathExpressions;

            List<ContentItem> items = new();

            foreach (var asset in assets.NoAllocEnumerate())
            {
                var path = asset.Path;

                foreach (var pathPattern in pathPatterns)
                {
                    var contentItem = pathPattern.Match(path, definition.PropertyDefinitions);
                    if (contentItem != null)
                    {
                        //If the item is assembly, populate the "related files extensions property".
                        if (contentItem.TryGetValue(ManagedCodeConventions.PropertyNames.ManagedAssembly, out _))
                        {
                            string? relatedFileExtensionsProperty = GetRelatedFileExtensionProperty(contentItem.Path, assets);
                            if (relatedFileExtensionsProperty is not null)
                            {
                                contentItem.Add("related", relatedFileExtensionsProperty);
                            }
                        }
                        items.Add(contentItem);
                        break;
                    }
                }
            }

            return items;
        }

        internal string? GetRelatedFileExtensionProperty(string assemblyPath, IEnumerable<Asset> assets)
        {
            //E.g. if path is "lib/net472/A.B.C.dll", the prefix will be "lib/net472/A.B.C."
            ReadOnlyMemory<char> assemblyPrefix = assemblyPath.AsMemory(0, assemblyPath.LastIndexOf('.') + 1);

            if (_assemblyRelatedExtensions != null && _assemblyRelatedExtensions.TryGetValue(assemblyPrefix, out string? relatedProperty))
            {
                return relatedProperty;
            }

            List<string>? relatedFileExtensionList = null;
            foreach (Asset asset in assets.NoAllocEnumerate())
            {
                if (asset.Path is not null)
                {
                    var extension = GetExtension(asset);

                    if (extension.Length > 0 &&
                        //Assembly properties are files with extensions ".dll", ".winmd", ".exe", see ManagedCodeConventions.
                        !ReadOnlyMemoryEquals(extension, Dll) &&
                        !ReadOnlyMemoryEquals(extension, Exe) &&
                        !ReadOnlyMemoryEquals(extension, Winmd) &&
                        !asset.Path.Equals(assemblyPath, StringComparison.OrdinalIgnoreCase) &&
                        //The prefix should match exactly (case sensitive), as file names are case sensitive on certain OSes.
                        //E.g. for lib/net472/A.B.C.dll and lib/net472/a.b.c.xml, if we generate related property '.xml', the related file path is not predictable on case sensitive OSes.
                        asset.Path.AsMemory().Span.StartsWith(assemblyPrefix.Span, StringComparison.Ordinal))
                    {
                        if (relatedFileExtensionList is null)
                        {
                            relatedFileExtensionList = new List<string>();
                        }
                        relatedFileExtensionList.Add(asset.Path.Substring(assemblyPrefix.Length - 1));
                    }
                }
            }

            // If no related files found.
            if (relatedFileExtensionList is null || relatedFileExtensionList.Count == 0)
            {
                if (_assemblyRelatedExtensions != null)
                {
                    _assemblyRelatedExtensions.TryAdd(assemblyPrefix, null);
                }
                return null;
            }
            else
            {
                relatedFileExtensionList.Sort();
                string relatedFileExtensionsProperty = string.Join(";", relatedFileExtensionList);
                if (_assemblyRelatedExtensions != null)
                {
                    _assemblyRelatedExtensions.TryAdd(assemblyPrefix, relatedFileExtensionsProperty);
                }
                return relatedFileExtensionsProperty;
            }

            static bool ReadOnlyMemoryEquals(ReadOnlyMemory<char> x, ReadOnlyMemory<char> y)
            {
                return x.Span.Equals(y.Span, StringComparison.OrdinalIgnoreCase);
            }

            static ReadOnlyMemory<char> GetExtension(Asset asset)
            {
                int lastIndexOfDot = asset.Path.LastIndexOf('.');
                if (lastIndexOfDot != -1)
                {
                    return asset.Path.AsMemory(lastIndexOfDot);
                }
                return default;
            }
        }

        /// <summary>
        /// False if the path would not match any patterns.
        /// </summary>
        private static bool IsValidAsset(string path)
        {
            // Verify that the file is not in the root. All patterns are for sub directories.
            for (var i = 1; i < path.Length; i++)
            {
                if (path[i] == '/')
                {
                    return true;
                }
            }

            return false;
        }

        internal class GroupComparer : IEqualityComparer<ContentItem>
        {
            public static readonly GroupComparer DefaultComparer = new GroupComparer();

            public int GetHashCode(ContentItem obj)
            {
                var hashCode = 0;
                if (obj._properties != null)
                {
                    foreach (var property in obj._properties)
                    {
#if NETFRAMEWORK || NETSTANDARD
                        hashCode ^= property.Key.GetHashCode();
#else
                        hashCode ^= property.Key.GetHashCode(StringComparison.Ordinal);
#endif
                        hashCode ^= property.Value.GetHashCode();
                    }
                }
                else
                {
                    if (obj._assembly != null)
                    {
                        hashCode ^= obj._assembly.GetHashCode();
                    }
                    if (obj._locale != null)
                    {
                        hashCode ^= obj._locale.GetHashCode();
                    }
                    if (obj._related != null)
                    {
                        hashCode ^= obj._related.GetHashCode();
                    }
                    if (obj._msbuild != null)
                    {
                        hashCode ^= obj._msbuild.GetHashCode();
                    }
                    if (obj._tfm != null)
                    {
                        hashCode ^= obj._tfm.GetHashCode();
                    }
                    if (obj._rid != null)
                    {
                        hashCode ^= obj._rid.GetHashCode();
                    }
                    if (obj._any != null)
                    {
                        hashCode ^= obj._any.GetHashCode();
                    }
                    if (obj._satelliteAssembly != null)
                    {
                        hashCode ^= obj._satelliteAssembly.GetHashCode();
                    }
                    if (obj._codeLanguage != null)
                    {
                        hashCode ^= obj._codeLanguage.GetHashCode();
                    }
                }
                return hashCode;
            }

            public bool Equals(ContentItem? x, ContentItem? y)
            {
                if (ReferenceEquals(x, y))
                {
                    return true;
                }
                if (x is null || y is null)
                {
                    return false;
                }

                if (x._properties == null && y._properties != null)
                {
                    return false;
                }

                if (x._properties != null && y._properties == null)
                {
                    return false;
                }

                if (x._properties?.Count != y._properties?.Count)
                {
                    return false;
                }

                if (x._properties != null && y._properties != null)
                {
                    foreach (var xProperty in x._properties)
                    {
                        object? yValue;
                        if (!y._properties.TryGetValue(xProperty.Key, out yValue))
                        {
                            return false;
                        }
                        if (!Equals(xProperty.Value, yValue))
                        {
                            return false;
                        }
                    }
                    foreach (var yProperty in y._properties)
                    {
                        object? xValue;
                        if (!x._properties.TryGetValue(yProperty.Key, out xValue))
                        {
                            return false;
                        }
                    }
                }
                else
                {
                    if (!EqualityUtility.EqualsWithNullCheck(x._assembly, y._assembly))
                    {
                        return false;
                    }
                    if (!EqualityUtility.EqualsWithNullCheck(x._locale, y._locale))
                    {
                        return false;
                    }
                    if (!EqualityUtility.EqualsWithNullCheck(x._related, y._related))
                    {
                        return false;
                    }
                    if (!EqualityUtility.EqualsWithNullCheck(x._msbuild, y._msbuild))
                    {
                        return false;
                    }
                    if (!EqualityUtility.EqualsWithNullCheck(x._tfm, y._tfm))
                    {
                        return false;
                    }
                    if (!EqualityUtility.EqualsWithNullCheck(x._rid, y._rid))
                    {
                        return false;
                    }
                    if (!EqualityUtility.EqualsWithNullCheck(x._any, y._any))
                    {
                        return false;
                    }
                    if (!EqualityUtility.EqualsWithNullCheck(x._satelliteAssembly, y._satelliteAssembly))
                    {
                        return false;
                    }
                    if (!EqualityUtility.EqualsWithNullCheck(x._codeLanguage, y._codeLanguage))
                    {
                        return false;
                    }
                }
                return true;
            }
        }
    }
}