File: PackageSourceMapping\SearchTree.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Configuration\NuGet.Configuration.csproj (NuGet.Configuration)
// 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.Globalization;
using System.Text;

namespace NuGet.Configuration
{
    internal class SearchTree
    {
        private readonly SearchNode _root;

        internal SearchTree(PackageSourceMapping configuration)
        {
            if (configuration == null)
            {
                throw new ArgumentNullException(nameof(configuration));
            }

            _root = new SearchNode();

            foreach (KeyValuePair<string, IReadOnlyList<string>> patternsPerSource in configuration.Patterns)
            {
                foreach (string namespaceId in patternsPerSource.Value)
                {
                    Add(patternsPerSource.Key, namespaceId);
                }
            }
        }

        private void Add(string packageSourceKey, string packagePattern)
        {
            SearchNode currentNode = _root;

            if (string.IsNullOrWhiteSpace(packagePattern))
            {
                throw new ArgumentException(Resources.Argument_Cannot_Be_Null_Empty_Or_WhiteSpaceOnly, nameof(packagePattern));
            }

            // To prevent from unwanted behaviour.
            if (packagePattern.Length > PackageSourceMapping.PackageIdMaxLength)
            {
                throw new ArgumentOutOfRangeException(nameof(packagePattern));
            }

            packageSourceKey = packageSourceKey.Trim();
            packagePattern = packagePattern.ToLower(CultureInfo.CurrentCulture).Trim();

            for (int i = 0; i < packagePattern.Length; i++)
            {
                char c = packagePattern[i];

                if (c == '*')
                {
                    // break here since seeing * means end of expression.
                    currentNode.IsGlobbing = true;
                    break;
                }

                if (!currentNode.Children.ContainsKey(c))
                {
                    currentNode.Children[c] = new SearchNode();
                }

                currentNode = currentNode.Children[c];
            }

            if (currentNode.PackageSources == null)
            {
                currentNode.PackageSources = new List<string>();
            }

            currentNode.PackageSources.Add(packageSourceKey);
        }

        /// <summary>
        /// Finds the pattern in <see cref="PackageSourceMapping.Patterns"/> which satisfies the <paramref name="term"/>.
        /// Patterns are all lowercase and include glob (`*`) characters.
        /// </summary>
        /// <param name="term">Term to search for in <see cref="PackageSourceMapping.Patterns"/>.</param>
        /// <returns>Found <see cref="PackageSourceMapping.Patterns"/> which satisfies the <paramref name="term"/>, or `null`.</returns>
        public string? SearchForPattern(string term)
        {
            return SearchPatternByTerm(term);
        }

        /// <summary>
        /// Get package source names with matching prefix "term" from package source mapping section.
        /// </summary>
        /// <param name="term">Search term. Cannot be null, empty, or whitespace only. </param>
        /// <returns>Package source names with matching prefix "term" from package source mapping section.</returns>
        /// <exception cref="ArgumentException"> if <paramref name="term"/> is null, empty, or whitespace only.</exception>
        public IReadOnlyList<string> GetConfiguredPackageSources(string term)
        {
            SearchNode? searchNodeResult = SearchNodeByTerm(term);
            if (searchNodeResult is null || searchNodeResult.PackageSources is null)
            {
                return Array.Empty<string>();
            }

            return searchNodeResult.PackageSources;
        }

        private SearchNode? SearchNodeByTerm(string term)
        {
            if (string.IsNullOrWhiteSpace(term))
            {
                throw new ArgumentException(Resources.Argument_Cannot_Be_Null_Empty_Or_WhiteSpaceOnly, nameof(term));
            }

            term = term.ToLower(CultureInfo.CurrentCulture).Trim();

            SearchNode currentNode = _root;
            SearchNode? longestMatchingPrefixNode = null;

            if (currentNode.IsGlobbing)
            {
                longestMatchingPrefixNode = currentNode;
            }

            int i = 0;

            for (; i < term.Length; i++)
            {
                char c = term[i];

                if (!currentNode.Children.TryGetValue(c, out SearchNode? child))
                {
                    break;
                }

                currentNode = child;

                if (currentNode.IsGlobbing)
                {
                    longestMatchingPrefixNode = currentNode;
                }
            }

            if (i == term.Length && currentNode.PackageSources != null)
            {
                return currentNode;
            }

            return longestMatchingPrefixNode;
        }

        private string? SearchPatternByTerm(string term)
        {
            if (string.IsNullOrWhiteSpace(term))
            {
                throw new ArgumentException(Resources.Argument_Cannot_Be_Null_Empty_Or_WhiteSpaceOnly, nameof(term));
            }

            term = term.ToLower(CultureInfo.CurrentCulture).Trim();

            StringBuilder sb = new StringBuilder();
            SearchNode currentNode = _root;

            if (currentNode.IsGlobbing)
            {
                return "*";
            }

            int i = 0;

            for (; i < term.Length; i++)
            {
                char c = term[i];

                if (!currentNode.Children.TryGetValue(c, out SearchNode? child))
                {
                    if (!currentNode.IsGlobbing)
                    {
                        return null;
                    }
                    break;
                }

                currentNode = child;

                sb.Append(c);
            }
            if (currentNode.IsGlobbing)
            {
                sb.Append('*');
            }

            return sb.ToString();
        }
    }
}