| | 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> |
| 10642 | 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 | | { |
| 867 | 29 | | get => _serverAddress; |
| | 30 | |
|
| | 31 | | init |
| 34 | 32 | | { |
| 34 | 33 | | if (Protocol is null) |
| 2 | 34 | | { |
| 2 | 35 | | throw new InvalidOperationException( |
| 2 | 36 | | $"Cannot set {nameof(ServerAddress)} on a relative service address."); |
| | 37 | | } |
| | 38 | |
|
| 32 | 39 | | if (value?.Protocol is Protocol newProtocol && newProtocol != Protocol) |
| 2 | 40 | | { |
| 2 | 41 | | throw new ArgumentException( |
| 2 | 42 | | $"The {nameof(ServerAddress)} must use the service address's protocol: '{Protocol}'.", |
| 2 | 43 | | nameof(value)); |
| | 44 | | } |
| | 45 | |
|
| 30 | 46 | | if (value is not null) |
| 28 | 47 | | { |
| 28 | 48 | | if (_params.Count > 0) |
| 2 | 49 | | { |
| 2 | 50 | | throw new InvalidOperationException( |
| 2 | 51 | | $"Cannot set {nameof(ServerAddress)} on a service address with parameters."); |
| | 52 | | } |
| 26 | 53 | | } |
| 2 | 54 | | else if (_altServerAddresses.Count > 0) |
| 2 | 55 | | { |
| 2 | 56 | | throw new InvalidOperationException( |
| 2 | 57 | | $"Cannot clear {nameof(ServerAddress)} when {nameof(AltServerAddresses)} is not empty."); |
| | 58 | | } |
| 26 | 59 | | _serverAddress = value; |
| 26 | 60 | | OriginalUri = null; |
| 26 | 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 | | { |
| 6598 | 68 | | get => _path; |
| | 69 | | init |
| 84 | 70 | | { |
| | 71 | | try |
| 84 | 72 | | { |
| 84 | 73 | | CheckPath(value); // make sure it's properly escaped |
| 82 | 74 | | Protocol?.CheckPath(value); // make sure the protocol is happy with this path |
| 80 | 75 | | } |
| 4 | 76 | | catch (FormatException exception) |
| 4 | 77 | | { |
| 4 | 78 | | throw new ArgumentException("Invalid path.", nameof(value), exception); |
| | 79 | | } |
| 80 | 80 | | _path = value; |
| 80 | 81 | | OriginalUri = null; |
| 80 | 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 | | { |
| 758 | 90 | | get => _altServerAddresses; |
| | 91 | |
|
| | 92 | | init |
| 12 | 93 | | { |
| 12 | 94 | | if (Protocol is null) |
| 0 | 95 | | { |
| 0 | 96 | | throw new InvalidOperationException( |
| 0 | 97 | | $"Cannot set {nameof(AltServerAddresses)} on a relative service address."); |
| | 98 | | } |
| | 99 | |
|
| 12 | 100 | | if (value.Count > 0) |
| 12 | 101 | | { |
| 12 | 102 | | if (_serverAddress is null) |
| 2 | 103 | | { |
| 2 | 104 | | throw new InvalidOperationException( |
| 2 | 105 | | $"Cannot set {nameof(AltServerAddresses)} when {nameof(ServerAddress)} is empty."); |
| | 106 | | } |
| | 107 | |
|
| 26 | 108 | | if (value.Any(e => e.Protocol != Protocol)) |
| 2 | 109 | | { |
| 2 | 110 | | throw new ArgumentException( |
| 2 | 111 | | $"The {nameof(AltServerAddresses)} server addresses must use the service address's protocol: '{P |
| 2 | 112 | | nameof(value)); |
| | 113 | | } |
| 8 | 114 | | } |
| | 115 | | // else, no need to check anything, an empty list is always fine. |
| | 116 | |
|
| 8 | 117 | | _altServerAddresses = value; |
| 8 | 118 | | OriginalUri = null; |
| 8 | 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 | | { |
| 519 | 127 | | get => _params; |
| | 128 | | init |
| 106 | 129 | | { |
| 106 | 130 | | if (Protocol is null) |
| 0 | 131 | | { |
| 0 | 132 | | throw new InvalidOperationException($"Cannot set {nameof(Params)} on a relative service address."); |
| | 133 | | } |
| | 134 | |
|
| | 135 | | try |
| 106 | 136 | | { |
| 106 | 137 | | CheckParams(value); // general checking (properly escape, no empty name) |
| 106 | 138 | | Protocol.CheckServiceAddressParams(value); // protocol-specific checking |
| 104 | 139 | | } |
| 2 | 140 | | catch (FormatException exception) |
| 2 | 141 | | { |
| 2 | 142 | | throw new ArgumentException("Invalid parameters.", nameof(value), exception); |
| | 143 | | } |
| | 144 | |
|
| 104 | 145 | | if (_serverAddress is not null && value.Count > 0) |
| 2 | 146 | | { |
| 2 | 147 | | throw new InvalidOperationException( |
| 2 | 148 | | $"Cannot set {nameof(Params)} on a service address with a serverAddress."); |
| | 149 | | } |
| | 150 | |
|
| 102 | 151 | | _params = value; |
| 102 | 152 | | OriginalUri = null; |
| 102 | 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 | | { |
| 6444 | 160 | | get => _fragment; |
| | 161 | | init |
| 8 | 162 | | { |
| 8 | 163 | | if (Protocol is null) |
| 2 | 164 | | { |
| 2 | 165 | | throw new InvalidOperationException($"Cannot set {nameof(Fragment)} on a relative service address."); |
| | 166 | | } |
| | 167 | |
|
| | 168 | | try |
| 6 | 169 | | { |
| 6 | 170 | | CheckFragment(value); // make sure it's properly escaped |
| 4 | 171 | | } |
| 2 | 172 | | catch (FormatException exception) |
| 2 | 173 | | { |
| 2 | 174 | | throw new ArgumentException("Invalid fragment.", nameof(value), exception); |
| | 175 | | } |
| | 176 | |
|
| 4 | 177 | | if (!Protocol.HasFragment && value.Length > 0) |
| 2 | 178 | | { |
| 2 | 179 | | throw new InvalidOperationException($"Cannot set {Fragment} on an {Protocol} service address."); |
| | 180 | | } |
| | 181 | |
|
| 2 | 182 | | _fragment = value; |
| 2 | 183 | | OriginalUri = null; |
| 2 | 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> |
| 1367 | 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 | |
|
| 14 | 198 | | private static readonly SearchValues<char> _notValidInFragment = SearchValues.Create("\"<>\\^`{|}"); |
| 14 | 199 | | private static readonly SearchValues<char> _notValidInParamName = SearchValues.Create("\"<>#&=\\^`{|}"); |
| 14 | 200 | | private static readonly SearchValues<char> _notValidInParamValue = SearchValues.Create("\"<>#&\\^`{|}"); |
| 14 | 201 | | private static readonly SearchValues<char> _notValidInPath = SearchValues.Create("\"<>#?\\^`{|}"); |
| | 202 | |
|
| 6376 | 203 | | private ImmutableList<ServerAddress> _altServerAddresses = ImmutableList<ServerAddress>.Empty; |
| 6376 | 204 | | private string _fragment = ""; |
| 6376 | 205 | | private ImmutableDictionary<string, string> _params = ImmutableDictionary<string, string>.Empty; |
| 6376 | 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> |
| 11178 | 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> |
| 750 | 215 | | public ServiceAddress(Uri uri) |
| 750 | 216 | | { |
| 750 | 217 | | if (uri.IsAbsoluteUri) |
| 716 | 218 | | { |
| 716 | 219 | | Protocol = Protocol.TryParse(uri.Scheme, out Protocol? protocol) ? protocol : |
| 716 | 220 | | throw new ArgumentException( |
| 716 | 221 | | $"Cannot create a service address with protocol '{uri.Scheme}'.", |
| 716 | 222 | | nameof(uri)); |
| | 223 | |
|
| | 224 | | // The AbsolutePath is empty for a URI such as "icerpc:?foo=bar" |
| 714 | 225 | | _path = uri.AbsolutePath.Length > 0 ? uri.AbsolutePath : "/"; |
| 714 | 226 | | _fragment = uri.Fragment.Length > 0 ? uri.Fragment[1..] : ""; // remove leading # |
| | 227 | |
|
| | 228 | | try |
| 714 | 229 | | { |
| 714 | 230 | | Protocol.CheckPath(_path); |
| 712 | 231 | | } |
| 2 | 232 | | catch (FormatException exception) |
| 2 | 233 | | { |
| 2 | 234 | | throw new ArgumentException($"Invalid path in {Protocol} URI.", nameof(uri), exception); |
| | 235 | | } |
| | 236 | |
|
| 712 | 237 | | if (!Protocol.HasFragment && _fragment.Length > 0) |
| 4 | 238 | | { |
| 4 | 239 | | throw new ArgumentException( |
| 4 | 240 | | $"Cannot create an {Protocol} service address with a fragment.", |
| 4 | 241 | | nameof(uri)); |
| | 242 | | } |
| | 243 | |
|
| 708 | 244 | | (ImmutableDictionary<string, string> queryParams, string? altServerValue, string? transport) = |
| 708 | 245 | | uri.ParseQuery(); |
| | 246 | |
|
| 708 | 247 | | if (uri.Authority.Length > 0) |
| 603 | 248 | | { |
| 603 | 249 | | if (uri.UserInfo.Length > 0) |
| 2 | 250 | | { |
| 2 | 251 | | throw new ArgumentException("Cannot create a server address with a user info.", nameof(uri)); |
| | 252 | | } |
| | 253 | |
|
| 601 | 254 | | string host = uri.IdnHost; |
| 601 | 255 | | Debug.Assert(host.Length > 0); // the IdnHost provided by Uri is never empty |
| | 256 | |
|
| 601 | 257 | | _serverAddress = new ServerAddress( |
| 601 | 258 | | Protocol, |
| 601 | 259 | | host, |
| 601 | 260 | | port: uri.Port == -1 ? Protocol.DefaultPort : checked((ushort)uri.Port), |
| 601 | 261 | | transport, |
| 601 | 262 | | queryParams); |
| | 263 | |
|
| 601 | 264 | | if (altServerValue is not null) |
| 83 | 265 | | { |
| | 266 | | // Split and parse recursively each serverAddress |
| 569 | 267 | | foreach (string serverAddressStr in altServerValue.Split(',')) |
| 163 | 268 | | { |
| 163 | 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. |
| 163 | 274 | | _altServerAddresses = _altServerAddresses.Add( |
| 163 | 275 | | new ServerAddress(new Uri(altUriString.Replace('$', '&')))); |
| 157 | 276 | | } |
| 77 | 277 | | } |
| 595 | 278 | | } |
| | 279 | | else |
| 105 | 280 | | { |
| 105 | 281 | | if (!_path.StartsWith('/')) |
| 2 | 282 | | { |
| 2 | 283 | | throw new ArgumentException( |
| 2 | 284 | | $"Invalid path in service address URI '{uri.OriginalString}'.", |
| 2 | 285 | | nameof(uri)); |
| | 286 | | } |
| | 287 | |
|
| 103 | 288 | | if (altServerValue is not null) |
| 2 | 289 | | { |
| 2 | 290 | | throw new ArgumentException( |
| 2 | 291 | | $"Invalid alt-server parameter in URI '{uri.OriginalString}'.", |
| 2 | 292 | | nameof(uri)); |
| | 293 | | } |
| | 294 | |
|
| | 295 | | try |
| 101 | 296 | | { |
| 101 | 297 | | Protocol.CheckServiceAddressParams(queryParams); |
| 97 | 298 | | } |
| 4 | 299 | | catch (FormatException exception) |
| 4 | 300 | | { |
| 4 | 301 | | throw new ArgumentException("Invalid parameters in URI.", nameof(uri), exception); |
| | 302 | | } |
| | 303 | |
|
| 97 | 304 | | Params = queryParams; |
| 97 | 305 | | } |
| 692 | 306 | | } |
| | 307 | | else |
| 34 | 308 | | { |
| | 309 | | // relative service address |
| 34 | 310 | | Protocol = null; |
| 34 | 311 | | _path = uri.ToString(); |
| | 312 | |
|
| | 313 | | try |
| 34 | 314 | | { |
| 34 | 315 | | CheckPath(_path); |
| 34 | 316 | | } |
| 0 | 317 | | catch (FormatException exception) |
| 0 | 318 | | { |
| 0 | 319 | | throw new ArgumentException("Invalid path in relative URI.", nameof(uri), exception); |
| | 320 | | } |
| 34 | 321 | | } |
| | 322 | |
|
| 726 | 323 | | OriginalUri = uri; |
| 726 | 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) |
| 256 | 333 | | { |
| 256 | 334 | | if (other is null) |
| 2 | 335 | | { |
| 2 | 336 | | return false; |
| | 337 | | } |
| 254 | 338 | | else if (ReferenceEquals(this, other)) |
| 0 | 339 | | { |
| 0 | 340 | | return true; |
| | 341 | | } |
| | 342 | |
|
| 254 | 343 | | if (Protocol != other.Protocol) |
| 2 | 344 | | { |
| 2 | 345 | | return false; |
| | 346 | | } |
| | 347 | |
|
| 252 | 348 | | if (Protocol is null) |
| 14 | 349 | | { |
| | 350 | | // Both service addresses are relative |
| 14 | 351 | | return Path == other.Path; |
| | 352 | | } |
| | 353 | |
|
| | 354 | | // Comparing 2 service addresses with the same protocol |
| 238 | 355 | | return Path == other.Path && |
| 238 | 356 | | Fragment == other.Fragment && |
| 238 | 357 | | ServerAddress == other.ServerAddress && |
| 238 | 358 | | AltServerAddresses.SequenceEqual(other.AltServerAddresses) && |
| 238 | 359 | | Params.DictionaryEqual(other.Params); |
| 256 | 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() |
| 282 | 365 | | { |
| 282 | 366 | | if (Protocol is null) |
| 18 | 367 | | { |
| 18 | 368 | | return Path.GetHashCode(StringComparison.Ordinal); |
| | 369 | | } |
| | 370 | |
|
| | 371 | | // We only hash a subset of the properties to keep GetHashCode reasonably fast. |
| 264 | 372 | | var hash = new HashCode(); |
| 264 | 373 | | hash.Add(Protocol); |
| 264 | 374 | | hash.Add(Path); |
| 264 | 375 | | hash.Add(Fragment); |
| 264 | 376 | | hash.Add(_serverAddress); |
| 264 | 377 | | hash.Add(_altServerAddresses.Count); |
| 264 | 378 | | return hash.ToHashCode(); |
| 282 | 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() |
| 450 | 384 | | { |
| 450 | 385 | | if (Protocol is null) |
| 33 | 386 | | { |
| 33 | 387 | | return Path; |
| | 388 | | } |
| 417 | 389 | | else if (OriginalUri is Uri uri) |
| 389 | 390 | | { |
| 389 | 391 | | return uri.ToString(); |
| | 392 | | } |
| | 393 | |
|
| | 394 | | // else, construct a string with a string builder. |
| | 395 | |
|
| 28 | 396 | | var sb = new StringBuilder(); |
| 28 | 397 | | bool firstOption = true; |
| | 398 | |
|
| 28 | 399 | | if (ServerAddress is ServerAddress serverAddress) |
| 4 | 400 | | { |
| 4 | 401 | | sb.AppendServerAddress(serverAddress, Path); |
| 4 | 402 | | firstOption = serverAddress.Params.Count == 0; |
| 4 | 403 | | } |
| | 404 | | else |
| 24 | 405 | | { |
| 24 | 406 | | sb.Append(Protocol); |
| 24 | 407 | | sb.Append(':'); |
| 24 | 408 | | sb.Append(Path); |
| 24 | 409 | | } |
| | 410 | |
|
| 28 | 411 | | if (AltServerAddresses.Count > 0) |
| 4 | 412 | | { |
| 4 | 413 | | StartQueryOption(sb, ref firstOption); |
| 4 | 414 | | sb.Append("alt-server="); |
| 24 | 415 | | for (int i = 0; i < AltServerAddresses.Count; ++i) |
| 8 | 416 | | { |
| 8 | 417 | | if (i > 0) |
| 4 | 418 | | { |
| 4 | 419 | | sb.Append(','); |
| 4 | 420 | | } |
| 8 | 421 | | sb.AppendServerAddress(AltServerAddresses[i], path: "", includeScheme: false, paramSeparator: '$'); |
| 8 | 422 | | } |
| 4 | 423 | | } |
| | 424 | |
|
| 92 | 425 | | foreach ((string name, string value) in Params) |
| 4 | 426 | | { |
| 4 | 427 | | StartQueryOption(sb, ref firstOption); |
| 4 | 428 | | sb.Append(name); |
| 4 | 429 | | if (value.Length > 0) |
| 4 | 430 | | { |
| 4 | 431 | | sb.Append('='); |
| 4 | 432 | | sb.Append(value); |
| 4 | 433 | | } |
| 4 | 434 | | } |
| | 435 | |
|
| 28 | 436 | | if (Fragment.Length > 0) |
| 4 | 437 | | { |
| 4 | 438 | | sb.Append('#'); |
| 4 | 439 | | sb.Append(Fragment); |
| 4 | 440 | | } |
| | 441 | |
|
| 28 | 442 | | return sb.ToString(); |
| | 443 | |
|
| | 444 | | static void StartQueryOption(StringBuilder sb, ref bool firstOption) |
| 8 | 445 | | { |
| 8 | 446 | | if (firstOption) |
| 4 | 447 | | { |
| 4 | 448 | | sb.Append('?'); |
| 4 | 449 | | firstOption = false; |
| 4 | 450 | | } |
| | 451 | | else |
| 4 | 452 | | { |
| 4 | 453 | | sb.Append('&'); |
| 4 | 454 | | } |
| 8 | 455 | | } |
| 450 | 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() => |
| 6 | 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) |
| 125 | 468 | | { |
| 499 | 469 | | foreach ((string name, string value) in @params) |
| 67 | 470 | | { |
| 67 | 471 | | if (!IsValidParamName(name)) |
| 6 | 472 | | { |
| 6 | 473 | | throw new FormatException($"Invalid parameter name '{name}'."); |
| | 474 | | } |
| 61 | 475 | | if (!IsValidParamValue(value)) |
| 4 | 476 | | { |
| 4 | 477 | | throw new FormatException($"Invalid parameter value '{value}'."); |
| | 478 | | } |
| 57 | 479 | | } |
| 115 | 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) |
| 244 | 489 | | { |
| 244 | 490 | | if (path.Length == 0 || path[0] != '/' || !IsValid(path, _notValidInPath)) |
| 8 | 491 | | { |
| 8 | 492 | | throw new FormatException( |
| 8 | 493 | | $"Invalid path '{path}'; a valid path starts with '/' and contains only unreserved characters, '%', and |
| | 494 | | } |
| 236 | 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> |
| 2713 | 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> |
| 37 | 506 | | internal ServiceAddress( |
| 37 | 507 | | Protocol protocol, |
| 37 | 508 | | string path, |
| 37 | 509 | | ServerAddress? serverAddress, |
| 37 | 510 | | ImmutableList<ServerAddress> altServerAddresses, |
| 37 | 511 | | ImmutableDictionary<string, string> serviceAddressParams, |
| 37 | 512 | | string fragment) |
| 37 | 513 | | { |
| 37 | 514 | | Protocol = protocol; |
| 37 | 515 | | _path = path; |
| 37 | 516 | | _serverAddress = serverAddress; |
| 37 | 517 | | _altServerAddresses = altServerAddresses; |
| 37 | 518 | | _params = serviceAddressParams; |
| 37 | 519 | | _fragment = fragment; |
| 37 | 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) |
| 6 | 528 | | { |
| 6 | 529 | | if (!IsValid(fragment, _notValidInFragment)) |
| 2 | 530 | | { |
| 2 | 531 | | throw new FormatException( |
| 2 | 532 | | $"Invalid fragment '{fragment}'; a valid fragment contains only unreserved characters, reserved characte |
| | 533 | | } |
| 4 | 534 | | } |
| | 535 | |
|
| | 536 | | private static bool IsValid(string s, SearchValues<char> invalidChars) |
| 3020 | 537 | | { |
| 3020 | 538 | | ReadOnlySpan<char> span = s.AsSpan(); |
| 3020 | 539 | | return span.IndexOfAnyExceptInRange(FirstValidChar, LastValidChar) == -1 && span.IndexOfAny(invalidChars) == -1; |
| 3020 | 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) => |
| 67 | 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) => |
| | 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) => |
| | 576 | | value is string valueStr ? new ServiceAddress(new Uri(valueStr)) : base.ConvertFrom(context, culture, value); |
| | 577 | | } |