< Summary

Information
Class: IceRpc.ServiceAddressTypeConverter
Assembly: IceRpc
File(s): /home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc/ServiceAddress.cs
Tag: 275_13775359185
Line coverage
100%
Covered lines: 2
Uncovered lines: 0
Coverable lines: 2
Total lines: 577
Line coverage: 100%
Branch coverage
50%
Covered branches: 2
Total branches: 4
Branch coverage: 50%
Method coverage
100%
Covered methods: 2
Total methods: 2
Method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
CanConvertFrom(...)50%22100%
ConvertFrom(...)50%22100%

File(s)

/home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc/ServiceAddress.cs

#LineLine coverage
 1// Copyright (c) ZeroC, Inc.
 2
 3using IceRpc.Internal;
 4using System.Buffers;
 5using System.Collections.Immutable;
 6using System.ComponentModel;
 7using System.Diagnostics;
 8using System.Globalization;
 9using System.Text;
 10
 11namespace IceRpc;
 12
 13/// <summary>Represents the URI of a service, parsed and processed for easier consumption by invokers. It's used to
 14/// construct an <see cref="OutgoingRequest" />.</summary>
 15// The properties of this class are sorted in URI order.
 16[TypeConverter(typeof(ServiceAddressTypeConverter))]
 17public sealed record class ServiceAddress
 18{
 19    /// <summary>Gets the protocol of this service address.</summary>
 20    /// <value>The protocol of the service address. It corresponds to the URI scheme and is <see langword="null" /> for
 21    /// a relative service address.</value>
 22    public Protocol? Protocol { get; }
 23
 24    /// <summary>Gets or initializes the main server address of this service address.</summary>
 25    /// <value>The main server address of this service address, or <see langword="null"/> if this service address has no
 26    /// server address.</value>
 27    public ServerAddress? ServerAddress
 28    {
 29        get => _serverAddress;
 30
 31        init
 32        {
 33            if (Protocol is null)
 34            {
 35                throw new InvalidOperationException(
 36                    $"Cannot set {nameof(ServerAddress)} on a relative service address.");
 37            }
 38
 39            if (value?.Protocol is Protocol newProtocol && newProtocol != Protocol)
 40            {
 41                throw new ArgumentException(
 42                    $"The {nameof(ServerAddress)} must use the service address's protocol: '{Protocol}'.",
 43                    nameof(value));
 44            }
 45
 46            if (value is not null)
 47            {
 48                if (_params.Count > 0)
 49                {
 50                    throw new InvalidOperationException(
 51                        $"Cannot set {nameof(ServerAddress)} on a service address with parameters.");
 52                }
 53            }
 54            else if (_altServerAddresses.Count > 0)
 55            {
 56                throw new InvalidOperationException(
 57                    $"Cannot clear {nameof(ServerAddress)} when {nameof(AltServerAddresses)} is not empty.");
 58            }
 59            _serverAddress = value;
 60            OriginalUri = null;
 61        }
 62    }
 63
 64    /// <summary>Gets or initializes the path of this service address.</summary>
 65    /// <value>The path of this service address. Defaults to <c>/</c>.</value>
 66    public string Path
 67    {
 68        get => _path;
 69        init
 70        {
 71            try
 72            {
 73                CheckPath(value); // make sure it's properly escaped
 74                Protocol?.CheckPath(value); // make sure the protocol is happy with this path
 75            }
 76            catch (FormatException exception)
 77            {
 78                throw new ArgumentException("Invalid path.", nameof(value), exception);
 79            }
 80            _path = value;
 81            OriginalUri = null;
 82        }
 83    }
 84
 85    /// <summary>Gets or initializes the secondary server addresses of this service address.</summary>
 86    /// <value>The secondary server addresses of this service address. Defaults to <see cref="ImmutableList{T}.Empty"
 87    /// />.</value>
 88    public ImmutableList<ServerAddress> AltServerAddresses
 89    {
 90        get => _altServerAddresses;
 91
 92        init
 93        {
 94            if (Protocol is null)
 95            {
 96                throw new InvalidOperationException(
 97                    $"Cannot set {nameof(AltServerAddresses)} on a relative service address.");
 98            }
 99
 100            if (value.Count > 0)
 101            {
 102                if (_serverAddress is null)
 103                {
 104                    throw new InvalidOperationException(
 105                        $"Cannot set {nameof(AltServerAddresses)} when {nameof(ServerAddress)} is empty.");
 106                }
 107
 108                if (value.Any(e => e.Protocol != Protocol))
 109                {
 110                    throw new ArgumentException(
 111                        $"The {nameof(AltServerAddresses)} server addresses must use the service address's protocol: '{P
 112                        nameof(value));
 113                }
 114            }
 115            // else, no need to check anything, an empty list is always fine.
 116
 117            _altServerAddresses = value;
 118            OriginalUri = null;
 119        }
 120    }
 121
 122    /// <summary>Gets or initializes the parameters of this service address.</summary>
 123    /// <value>The params dictionary. Always empty if <see cref="ServerAddress" /> is not <see langword="null"/>.
 124    /// Defaults to <see cref="ImmutableDictionary{TKey, TValue}.Empty" />.</value>.
 125    public ImmutableDictionary<string, string> Params
 126    {
 127        get => _params;
 128        init
 129        {
 130            if (Protocol is null)
 131            {
 132                throw new InvalidOperationException($"Cannot set {nameof(Params)} on a relative service address.");
 133            }
 134
 135            try
 136            {
 137                CheckParams(value); // general checking (properly escape, no empty name)
 138                Protocol.CheckServiceAddressParams(value); // protocol-specific checking
 139            }
 140            catch (FormatException exception)
 141            {
 142                throw new ArgumentException("Invalid parameters.", nameof(value), exception);
 143            }
 144
 145            if (_serverAddress is not null && value.Count > 0)
 146            {
 147                throw new InvalidOperationException(
 148                    $"Cannot set {nameof(Params)} on a service address with a serverAddress.");
 149            }
 150
 151            _params = value;
 152            OriginalUri = null;
 153        }
 154    }
 155
 156    /// <summary>Gets or initializes the fragment.</summary>
 157    /// <value>The fragment of this service address. Defaults to an empty string.</value>
 158    public string Fragment
 159    {
 160        get => _fragment;
 161        init
 162        {
 163            if (Protocol is null)
 164            {
 165                throw new InvalidOperationException($"Cannot set {nameof(Fragment)} on a relative service address.");
 166            }
 167
 168            try
 169            {
 170                CheckFragment(value); // make sure it's properly escaped
 171            }
 172            catch (FormatException exception)
 173            {
 174                throw new ArgumentException("Invalid fragment.", nameof(value), exception);
 175            }
 176
 177            if (!Protocol.HasFragment && value.Length > 0)
 178            {
 179                throw new InvalidOperationException($"Cannot set {Fragment} on an {Protocol} service address.");
 180            }
 181
 182            _fragment = value;
 183            OriginalUri = null;
 184        }
 185    }
 186
 187    /// <summary>Gets the URI used to create this service address.</summary>
 188    /// <value>The <see cref="Uri" /> of this service address if it was constructed from a URI and if URI-derived
 189    /// properties have not been updated. The setting of a URI-derived property such as <see cref="ServerAddress" />
 190    /// sets <see cref="OriginalUri" /> to <see langword="null"/>.</value>
 191    public Uri? OriginalUri { get; private set; }
 192
 193    // The printable ASCII character range is x20 (space) to x7E inclusive. Space is an invalid character in path,
 194    // fragment, etc. in addition to the invalid characters in the _notValidInXXX search values.
 195    private const char FirstValidChar = '\x21';
 196    private const char LastValidChar = '\x7E';
 197
 198    private static readonly SearchValues<char> _notValidInFragment = SearchValues.Create("\"<>\\^`{|}");
 199    private static readonly SearchValues<char> _notValidInParamName = SearchValues.Create("\"<>#&=\\^`{|}");
 200    private static readonly SearchValues<char> _notValidInParamValue = SearchValues.Create("\"<>#&\\^`{|}");
 201    private static readonly SearchValues<char> _notValidInPath = SearchValues.Create("\"<>#?\\^`{|}");
 202
 203    private ImmutableList<ServerAddress> _altServerAddresses = ImmutableList<ServerAddress>.Empty;
 204    private string _fragment = "";
 205    private ImmutableDictionary<string, string> _params = ImmutableDictionary<string, string>.Empty;
 206    private string _path = "/";
 207    private ServerAddress? _serverAddress;
 208
 209    /// <summary>Constructs a service address from a protocol.</summary>
 210    /// <param name="protocol">The protocol, or <see langword="null" /> for a relative service address.</param>
 211    public ServiceAddress(Protocol? protocol = null) => Protocol = protocol;
 212
 213    /// <summary>Constructs a service address from a URI.</summary>
 214    /// <param name="uri">The Uri.</param>
 215    public ServiceAddress(Uri uri)
 216    {
 217        if (uri.IsAbsoluteUri)
 218        {
 219            Protocol = Protocol.TryParse(uri.Scheme, out Protocol? protocol) ? protocol :
 220                throw new ArgumentException(
 221                    $"Cannot create a service address with protocol '{uri.Scheme}'.",
 222                    nameof(uri));
 223
 224            // The AbsolutePath is empty for a URI such as "icerpc:?foo=bar"
 225            _path = uri.AbsolutePath.Length > 0 ? uri.AbsolutePath : "/";
 226            _fragment = uri.Fragment.Length > 0 ? uri.Fragment[1..] : ""; // remove leading #
 227
 228            try
 229            {
 230                Protocol.CheckPath(_path);
 231            }
 232            catch (FormatException exception)
 233            {
 234                throw new ArgumentException($"Invalid path in {Protocol} URI.", nameof(uri), exception);
 235            }
 236
 237            if (!Protocol.HasFragment && _fragment.Length > 0)
 238            {
 239                throw new ArgumentException(
 240                    $"Cannot create an {Protocol} service address with a fragment.",
 241                    nameof(uri));
 242            }
 243
 244            (ImmutableDictionary<string, string> queryParams, string? altServerValue, string? transport) =
 245                uri.ParseQuery();
 246
 247            if (uri.Authority.Length > 0)
 248            {
 249                if (uri.UserInfo.Length > 0)
 250                {
 251                    throw new ArgumentException("Cannot create a server address with a user info.", nameof(uri));
 252                }
 253
 254                string host = uri.IdnHost;
 255                Debug.Assert(host.Length > 0); // the IdnHost provided by Uri is never empty
 256
 257                _serverAddress = new ServerAddress(
 258                    Protocol,
 259                    host,
 260                    port: uri.Port == -1 ? Protocol.DefaultPort : checked((ushort)uri.Port),
 261                    transport,
 262                    queryParams);
 263
 264                if (altServerValue is not null)
 265                {
 266                    // Split and parse recursively each serverAddress
 267                    foreach (string serverAddressStr in altServerValue.Split(','))
 268                    {
 269                        string altUriString = $"{uri.Scheme}://{serverAddressStr}";
 270
 271                        // The separator for server address parameters in alt-server is $, so we replace these '$'
 272                        // by '&' before sending the string (Uri) to the ServerAddress constructor which uses '&' as
 273                        // separator.
 274                        _altServerAddresses = _altServerAddresses.Add(
 275                            new ServerAddress(new Uri(altUriString.Replace('$', '&'))));
 276                    }
 277                }
 278            }
 279            else
 280            {
 281                if (!_path.StartsWith('/'))
 282                {
 283                    throw new ArgumentException(
 284                        $"Invalid path in service address URI '{uri.OriginalString}'.",
 285                        nameof(uri));
 286                }
 287
 288                if (altServerValue is not null)
 289                {
 290                    throw new ArgumentException(
 291                        $"Invalid alt-server parameter in URI '{uri.OriginalString}'.",
 292                        nameof(uri));
 293                }
 294
 295                try
 296                {
 297                    Protocol.CheckServiceAddressParams(queryParams);
 298                }
 299                catch (FormatException exception)
 300                {
 301                    throw new ArgumentException("Invalid parameters in URI.", nameof(uri), exception);
 302                }
 303
 304                Params = queryParams;
 305            }
 306        }
 307        else
 308        {
 309            // relative service address
 310            Protocol = null;
 311            _path = uri.ToString();
 312
 313            try
 314            {
 315                CheckPath(_path);
 316            }
 317            catch (FormatException exception)
 318            {
 319                throw new ArgumentException("Invalid path in relative URI.", nameof(uri), exception);
 320            }
 321        }
 322
 323        OriginalUri = uri;
 324    }
 325
 326    /// <summary>Determines whether the specified <see cref="ServiceAddress"/> is equal to the current
 327    /// <see cref="ServiceAddress"/>.</summary>
 328    /// <param name="other">The <see cref="ServiceAddress"/> to compare with the current <see cref="ServiceAddress"/>.
 329    /// </param>
 330    /// <returns><see langword="true"/> if the specified <see cref="ServiceAddress"/> is equal to the current
 331    /// <see cref="ServiceAddress"/>; otherwise, <see langword="false"/>.</returns>
 332    public bool Equals(ServiceAddress? other)
 333    {
 334        if (other is null)
 335        {
 336            return false;
 337        }
 338        else if (ReferenceEquals(this, other))
 339        {
 340            return true;
 341        }
 342
 343        if (Protocol != other.Protocol)
 344        {
 345            return false;
 346        }
 347
 348        if (Protocol is null)
 349        {
 350            // Both service addresses are relative
 351            return Path == other.Path;
 352        }
 353
 354        // Comparing 2 service addresses with the same protocol
 355        return Path == other.Path &&
 356            Fragment == other.Fragment &&
 357            ServerAddress == other.ServerAddress &&
 358            AltServerAddresses.SequenceEqual(other.AltServerAddresses) &&
 359            Params.DictionaryEqual(other.Params);
 360    }
 361
 362    /// <summary>Serves as the default hash function.</summary>
 363    /// <returns>A hash code for the current <see cref="ServiceAddress"/>.</returns>
 364    public override int GetHashCode()
 365    {
 366        if (Protocol is null)
 367        {
 368            return Path.GetHashCode(StringComparison.Ordinal);
 369        }
 370
 371        // We only hash a subset of the properties to keep GetHashCode reasonably fast.
 372        var hash = new HashCode();
 373        hash.Add(Protocol);
 374        hash.Add(Path);
 375        hash.Add(Fragment);
 376        hash.Add(_serverAddress);
 377        hash.Add(_altServerAddresses.Count);
 378        return hash.ToHashCode();
 379    }
 380
 381    /// <summary>Converts this service address into a string.</summary>
 382    /// <returns>The string representation of this service address.</returns>
 383    public override string ToString()
 384    {
 385        if (Protocol is null)
 386        {
 387            return Path;
 388        }
 389        else if (OriginalUri is Uri uri)
 390        {
 391            return uri.ToString();
 392        }
 393
 394        // else, construct a string with a string builder.
 395
 396        var sb = new StringBuilder();
 397        bool firstOption = true;
 398
 399        if (ServerAddress is ServerAddress serverAddress)
 400        {
 401            sb.AppendServerAddress(serverAddress, Path);
 402            firstOption = serverAddress.Params.Count == 0;
 403        }
 404        else
 405        {
 406            sb.Append(Protocol);
 407            sb.Append(':');
 408            sb.Append(Path);
 409        }
 410
 411        if (AltServerAddresses.Count > 0)
 412        {
 413            StartQueryOption(sb, ref firstOption);
 414            sb.Append("alt-server=");
 415            for (int i = 0; i < AltServerAddresses.Count; ++i)
 416            {
 417                if (i > 0)
 418                {
 419                    sb.Append(',');
 420                }
 421                sb.AppendServerAddress(AltServerAddresses[i], path: "", includeScheme: false, paramSeparator: '$');
 422            }
 423        }
 424
 425        foreach ((string name, string value) in Params)
 426        {
 427            StartQueryOption(sb, ref firstOption);
 428            sb.Append(name);
 429            if (value.Length > 0)
 430            {
 431                sb.Append('=');
 432                sb.Append(value);
 433            }
 434        }
 435
 436        if (Fragment.Length > 0)
 437        {
 438            sb.Append('#');
 439            sb.Append(Fragment);
 440        }
 441
 442        return sb.ToString();
 443
 444        static void StartQueryOption(StringBuilder sb, ref bool firstOption)
 445        {
 446            if (firstOption)
 447            {
 448                sb.Append('?');
 449                firstOption = false;
 450            }
 451            else
 452            {
 453                sb.Append('&');
 454            }
 455        }
 456    }
 457
 458    /// <summary>Converts this service address into a Uri.</summary>
 459    /// <returns>An Uri representing this service address.</returns>
 460    public Uri ToUri() =>
 461        OriginalUri ?? (Protocol is null ? new Uri(Path, UriKind.Relative) : new Uri(ToString(), UriKind.Absolute));
 462
 463    /// <summary>Checks if <paramref name="params" /> contains properly escaped names and values.</summary>
 464    /// <param name="params">The dictionary to check.</param>
 465    /// <exception cref="FormatException">Thrown if the dictionary is not valid.</exception>
 466    /// <remarks>A dictionary returned by <see cref="UriExtensions.ParseQuery" /> is properly escaped.</remarks>
 467    internal static void CheckParams(ImmutableDictionary<string, string> @params)
 468    {
 469        foreach ((string name, string value) in @params)
 470        {
 471            if (!IsValidParamName(name))
 472            {
 473                throw new FormatException($"Invalid parameter name '{name}'.");
 474            }
 475            if (!IsValidParamValue(value))
 476            {
 477                throw new FormatException($"Invalid parameter value '{value}'.");
 478            }
 479        }
 480    }
 481
 482    /// <summary>Checks if <paramref name="path" /> is a properly escaped URI absolute path, i.e. that it starts
 483    /// with a <c>/</c> and contains only unreserved characters, <c>%</c>, and reserved characters other than
 484    /// <c>?</c> and <c>#</c>.</summary>
 485    /// <param name="path">The path to check.</param>
 486    /// <exception cref="FormatException">Thrown if the path is not valid.</exception>
 487    /// <remarks>The absolute path of a URI with a supported protocol satisfies these requirements.</remarks>
 488    internal static void CheckPath(string path)
 489    {
 490        if (path.Length == 0 || path[0] != '/' || !IsValid(path, _notValidInPath))
 491        {
 492            throw new FormatException(
 493                $"Invalid path '{path}'; a valid path starts with '/' and contains only unreserved characters, '%', and 
 494        }
 495    }
 496
 497    /// <summary>Checks if <paramref name="value" /> contains only unreserved characters, <c>%</c>, and reserved
 498    /// characters other than <c>#</c> and <c>&#38;</c>.</summary>
 499    /// <param name="value">The value to check.</param>
 500    /// <returns><see langword="true" /> if <paramref name="value" /> is a valid parameter value; otherwise,
 501    /// <see langword="false" />.</returns>
 502    internal static bool IsValidParamValue(string value) => IsValid(value, _notValidInParamValue);
 503
 504    /// <summary>"unchecked" constructor used by the Slice decoder when decoding a Slice1 encoded service address.
 505    /// </summary>
 506    internal ServiceAddress(
 507        Protocol protocol,
 508        string path,
 509        ServerAddress? serverAddress,
 510        ImmutableList<ServerAddress> altServerAddresses,
 511        ImmutableDictionary<string, string> serviceAddressParams,
 512        string fragment)
 513    {
 514        Protocol = protocol;
 515        _path = path;
 516        _serverAddress = serverAddress;
 517        _altServerAddresses = altServerAddresses;
 518        _params = serviceAddressParams;
 519        _fragment = fragment;
 520    }
 521
 522    /// <summary>Checks if <paramref name="fragment" /> is a properly escaped URI fragment, i.e. it contains only
 523    /// unreserved characters, reserved characters, and '%'.</summary>
 524    /// <param name="fragment">The fragment to check.</param>
 525    /// <exception cref="FormatException">Thrown if the fragment is not valid.</exception>
 526    /// <remarks>The fragment of a URI with a supported protocol satisfies these requirements.</remarks>
 527    private static void CheckFragment(string fragment)
 528    {
 529        if (!IsValid(fragment, _notValidInFragment))
 530        {
 531            throw new FormatException(
 532                $"Invalid fragment '{fragment}'; a valid fragment contains only unreserved characters, reserved characte
 533        }
 534    }
 535
 536    private static bool IsValid(string s, SearchValues<char> invalidChars)
 537    {
 538        ReadOnlySpan<char> span = s.AsSpan();
 539        return span.IndexOfAnyExceptInRange(FirstValidChar, LastValidChar) == -1 && span.IndexOfAny(invalidChars) == -1;
 540    }
 541
 542    /// <summary>Checks if <paramref name="name" /> is not empty, not equal to <c>alt-server</c> nor equal to
 543    /// <c>transport</c> and contains only unreserved characters, <c>%</c>, or reserved characters other than <c>#</c>,
 544    /// <c>&#38;</c> and <c>=</c>.</summary>
 545    /// <param name="name">The name to check.</param>
 546    /// <returns><see langword="true" /> if <paramref name="name" /> is a valid parameter name; otherwise,
 547    /// <see langword="false" />.</returns>
 548    /// <remarks>The range of valid names is much larger than the range of names you should use. For example, you
 549    /// should avoid parameter names with a <c>%</c> or <c>$</c> character, even though these characters are valid
 550    /// in a name.</remarks>
 551    private static bool IsValidParamName(string name) =>
 552        name.Length > 0 && name != "alt-server" && name != "transport" && IsValid(name, _notValidInParamName);
 553}
 554
 555/// <summary>The service address type converter specifies how to convert a string to a service address. It's used by
 556/// sub-systems such as the Microsoft ConfigurationBinder to bind string values to ServiceAddress properties.</summary>
 557public class ServiceAddressTypeConverter : TypeConverter
 558{
 559    /// <summary>Returns whether this converter can convert an object of the given type into a
 560    /// <see cref="ServiceAddress"/> object, using the specified context.</summary>
 561    /// <param name="context">An <see cref="ITypeDescriptorContext"/> that provides a format context.</param>
 562    /// <param name="sourceType">A <see cref="Type"/> that represents the type you want to convert from.</param>
 563    /// <returns><see langword="true"/>if this converter can perform the conversion; otherwise, <see langword="false"/>.
 564    /// </returns>
 565    public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) =>
 92566        sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
 567
 568    /// <summary>Converts the given object into a <see cref="ServiceAddress"/> object, using the specified context and c
 569    /// information.</summary>
 570    /// <param name="context">An <see cref="ITypeDescriptorContext"/> that provides a format context.</param>
 571    /// <param name="culture">The <see cref="CultureInfo"/> to use as the current culture.</param>
 572    /// <param name="value">The <see cref="object "/> to convert.</param>
 573    /// <returns>An <see cref="object "/> that represents the converted <see cref="ServiceAddress"/>.</returns>
 574    /// <remarks><see cref="TypeConverter"/>.</remarks>
 575    public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) =>
 92576        value is string valueStr ? new ServiceAddress(new Uri(valueStr)) : base.ConvertFrom(context, culture, value);
 577}