< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 132
Coverable lines: 132
Total lines: 265
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 44
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.ctor(...)100%110%
.ctor(...)100%110%
.ctor(...)100%110%
.ctor(...)0%220%
Contains(...)0%440%
Merge(...)100%110%
Merge(...)100%110%
AddIfNew(...)0%880%
AddIfNew(...)0%220%
AddIfNew(...)0%220%
AddIfNew(...)0%220%
GetCountOrOne(...)0%220%
NormalizeServiceName(...)0%22220%

File(s)

https://raw.githubusercontent.com/dotnet/runtime/811a7eabb75c42db53440e8ba3f60c07511cfd1f/src/libraries/System.Net.Security/src/System/Security/Authentication/ExtendedProtection/ServiceNameCollection.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Collections;
 5using System.Collections.Generic;
 6using System.Diagnostics;
 7using System.Diagnostics.CodeAnalysis;
 8using System.Globalization;
 9using System.Net;
 10
 11namespace System.Security.Authentication.ExtendedProtection
 12{
 13    public class ServiceNameCollection : ReadOnlyCollectionBase
 14    {
 015        public ServiceNameCollection(ICollection items)
 016        {
 017            ArgumentNullException.ThrowIfNull(items);
 18
 19            // Normalize and filter for duplicates.
 020            AddIfNew(items, expectStrings: true);
 021        }
 22
 23        /// <summary>
 24        /// Merges <paramref name="list"/> and <paramref name="serviceName"/> into a new collection.
 25        /// </summary>
 26        private ServiceNameCollection(IList list, string serviceName)
 027            : this(list, additionalCapacity: 1)
 028        {
 029            AddIfNew(serviceName);
 030        }
 31
 32        /// <summary>
 33        /// Merges <paramref name="list"/> and <paramref name="serviceNames"/> into a new collection.
 34        /// </summary>
 35        private ServiceNameCollection(IList list, IEnumerable serviceNames)
 036            : this(list, additionalCapacity: GetCountOrOne(serviceNames))
 037        {
 38            // We have a pretty bad performance here: O(n^2), but since service name lists should
 39            // be small (<<50) and Merge() should not be called frequently, this shouldn't be an issue.
 040            AddIfNew(serviceNames, expectStrings: false);
 041        }
 42
 043        private ServiceNameCollection(IList list, int additionalCapacity)
 044        {
 045            Debug.Assert(list != null);
 046            Debug.Assert(additionalCapacity >= 0);
 47
 048            foreach (string? item in list)
 049            {
 050                InnerList.Add(item);
 051            }
 052        }
 53
 54        public bool Contains(string? searchServiceName)
 055        {
 056            string? searchName = NormalizeServiceName(searchServiceName);
 57
 058            foreach (string serviceName in InnerList)
 059            {
 060                if (string.Equals(serviceName, searchName, StringComparison.OrdinalIgnoreCase))
 061                {
 062                    return true;
 63                }
 064            }
 65
 066            return false;
 067        }
 68
 069        public ServiceNameCollection Merge(string serviceName) => new ServiceNameCollection(InnerList, serviceName);
 70
 071        public ServiceNameCollection Merge(IEnumerable serviceNames) => new ServiceNameCollection(InnerList, serviceName
 72
 73        /// <summary>
 74        /// Normalize, check for duplicates, and add each unique value.
 75        /// </summary>
 76        private void AddIfNew(IEnumerable serviceNames, bool expectStrings)
 077        {
 078            List<string>? list = serviceNames as List<string>;
 079            if (list != null)
 080            {
 081                AddIfNew(list);
 082                return;
 83            }
 84
 085            ServiceNameCollection? snc = serviceNames as ServiceNameCollection;
 086            if (snc != null)
 087            {
 088                AddIfNew(snc.InnerList);
 089                return;
 90            }
 91
 92            // NullReferenceException is thrown when serviceNames is null,
 93            // which is consistent with the behavior of the .NET Framework.
 094            foreach (object item in serviceNames)
 095            {
 96                // To match the behavior of the .NET Framework, when an item
 97                // in the collection is not a string:
 98                //  - Throw InvalidCastException when expectStrings is true.
 99                //  - Throw ArgumentException when expectStrings is false.
 0100                AddIfNew(expectStrings ? (string)item : (item as string)!);
 0101            }
 0102        }
 103
 104        /// <summary>
 105        /// Normalize, check for duplicates, and add each unique value.
 106        /// </summary>
 107        private void AddIfNew(List<string> serviceNames)
 0108        {
 0109            Debug.Assert(serviceNames != null);
 110
 0111            foreach (string serviceName in serviceNames)
 0112            {
 0113                AddIfNew(serviceName);
 0114            }
 0115        }
 116
 117        /// <summary>
 118        /// Normalize, check for duplicates, and add each unique value.
 119        /// </summary>
 120        private void AddIfNew(IList serviceNames)
 0121        {
 0122            Debug.Assert(serviceNames != null);
 123
 0124            foreach (string serviceName in serviceNames)
 0125            {
 0126                AddIfNew(serviceName);
 0127            }
 0128        }
 129
 130        /// <summary>
 131        /// Normalize, check for duplicates, and add if the value is unique.
 132        /// </summary>
 133        private void AddIfNew(string serviceName)
 0134        {
 0135            ArgumentException.ThrowIfNullOrEmpty(serviceName);
 136
 0137            serviceName = NormalizeServiceName(serviceName);
 138
 0139            if (!Contains(serviceName))
 0140            {
 0141                InnerList.Add(serviceName);
 0142            }
 0143        }
 144
 145        /// <summary>
 146        /// Gets the collection Count, if available, otherwise 1.
 147        /// </summary>
 148        private static int GetCountOrOne(IEnumerable collection)
 0149        {
 0150            ICollection<string>? c = collection as ICollection<string>;
 0151            return c != null ? c.Count : 1;
 0152        }
 153
 154        // Normalizes any punycode to Unicode in an Service Name (SPN) host.
 155        // If the algorithm fails at any point then the original input is returned.
 156        // ServiceName is in one of the following forms:
 157        // prefix/host
 158        // prefix/host:port
 159        // prefix/host/DistinguishedName
 160        // prefix/host:port/DistinguishedName
 161        [return: NotNullIfNotNull(nameof(inputServiceName))]
 162        private static string? NormalizeServiceName(string? inputServiceName)
 0163        {
 0164            if (string.IsNullOrWhiteSpace(inputServiceName))
 0165            {
 0166                return inputServiceName;
 167            }
 168
 169            // Separate out the prefix
 0170            int slashIndex = inputServiceName.IndexOf('/');
 0171            if (slashIndex < 0)
 0172            {
 0173                return inputServiceName;
 174            }
 175
 0176            ReadOnlySpan<char> prefix = inputServiceName.AsSpan(0, slashIndex + 1); // Includes slash
 0177            string hostPortAndDistinguisher = inputServiceName.Substring(slashIndex + 1); // Excludes slash
 178
 0179            if (hostPortAndDistinguisher.Length == 0)
 0180            {
 0181                return inputServiceName;
 182            }
 183
 0184            ReadOnlySpan<char> host = hostPortAndDistinguisher;
 0185            ReadOnlySpan<char> port = default;
 0186            ReadOnlySpan<char> distinguisher = default;
 187
 188            // Check for the absence of a port or distinguisher.
 0189            UriHostNameType hostType = Uri.CheckHostName(hostPortAndDistinguisher);
 0190            if (hostType == UriHostNameType.Unknown)
 0191            {
 0192                ReadOnlySpan<char> hostAndPort = hostPortAndDistinguisher;
 193
 194                // Check for distinguisher.
 0195                int nextSlashIndex = hostPortAndDistinguisher.IndexOf('/');
 0196                if (nextSlashIndex >= 0)
 0197                {
 198                    // host:port/distinguisher or host/distinguisher
 0199                    hostAndPort = hostPortAndDistinguisher.AsSpan(0, nextSlashIndex); // Excludes Slash
 0200                    distinguisher = hostPortAndDistinguisher.AsSpan(nextSlashIndex); // Includes Slash
 0201                    host = hostAndPort; // We don't know if there is a port yet.
 202                    // No need to validate the distinguisher.
 0203                }
 204
 205                // Check for port.
 0206                int colonIndex = hostAndPort.LastIndexOf(':'); // Allow IPv6 addresses.
 0207                if (colonIndex >= 0)
 0208                {
 209                    // host:port
 0210                    host = hostAndPort.Slice(0, colonIndex); // Excludes colon
 0211                    port = hostAndPort.Slice(colonIndex + 1); // Excludes colon
 212
 213                    // Loosely validate the port just to make sure it was a port and not something else.
 0214                    if (!ushort.TryParse(port, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
 0215                    {
 0216                        return inputServiceName;
 217                    }
 218
 219                    // Re-include the colon for the final output.  Do not change the port format.
 0220                    port = hostAndPort.Slice(colonIndex);
 0221                }
 222
 223                // Re-validate the host.
 0224                hostType = Uri.CheckHostName(
 0225                    host.Length == hostPortAndDistinguisher.Length ?
 0226                        hostPortAndDistinguisher :
 0227                        host.ToString());
 0228            }
 229
 0230            if (hostType != UriHostNameType.Dns)
 0231            {
 232                // UriHostNameType.IPv4, UriHostNameType.IPv6: Do not normalize IPv4/6 hosts.
 233                // UriHostNameType.Basic: This is never returned by CheckHostName today
 234                // UriHostNameType.Unknown: Nothing recognizable to normalize
 235                // default Some new UriHostNameType?
 0236                return inputServiceName;
 237            }
 238
 239            // Now we have a valid DNS host, normalize it.
 240
 241            Uri? constructedUri;
 242
 243            // We need to avoid any unexpected exceptions on this code path.
 244            const string HttpSchemeAndDelimiter = UriScheme.Http + UriScheme.SchemeDelimiter;
 0245            if (!Uri.TryCreate(string.Concat(HttpSchemeAndDelimiter, host), UriKind.Absolute, out constructedUri))
 0246            {
 0247                return inputServiceName;
 248            }
 249
 0250            string normalizedHost = constructedUri.GetComponents(
 0251                UriComponents.NormalizedHost, UriFormat.SafeUnescaped);
 252
 0253            string normalizedServiceName = string.Concat(prefix, normalizedHost, port, distinguisher);
 254
 255            // Don't return the new one unless we absolutely have to.  It may have only changed casing.
 0256            if (string.Equals(inputServiceName, normalizedServiceName, StringComparison.OrdinalIgnoreCase))
 0257            {
 0258                return inputServiceName;
 259            }
 260
 0261            return normalizedServiceName;
 0262        }
 263    }
 264}
 265