| | | 1 | | // Copyright (c) ZeroC, Inc. |
| | | 2 | | |
| | | 3 | | using IceRpc.Internal; |
| | | 4 | | using System.Buffers; |
| | | 5 | | using System.Collections.Immutable; |
| | | 6 | | using System.ComponentModel; |
| | | 7 | | using System.Diagnostics; |
| | | 8 | | using System.Globalization; |
| | | 9 | | using System.Text; |
| | | 10 | | |
| | | 11 | | namespace 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))] |
| | | 17 | | public 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>&</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>&</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> |
| | | 557 | | public 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) => |
| | 92 | 566 | | 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) => |
| | 92 | 576 | | value is string valueStr ? new ServiceAddress(new Uri(valueStr)) : base.ConvertFrom(context, culture, value); |
| | | 577 | | } |