diff --git a/src/Orleans.Core.Abstractions/IDs/SiloAddress.cs b/src/Orleans.Core.Abstractions/IDs/SiloAddress.cs index 44fd5b00798..6976212c85c 100644 --- a/src/Orleans.Core.Abstractions/IDs/SiloAddress.cs +++ b/src/Orleans.Core.Abstractions/IDs/SiloAddress.cs @@ -21,7 +21,12 @@ namespace Orleans.Runtime [JsonConverter(typeof(SiloAddressConverter))] [DebuggerDisplay("SiloAddress {ToString()}")] [SuppressReferenceTracking] - public sealed class SiloAddress : IEquatable, IComparable, ISpanFormattable + public sealed class SiloAddress : + IEquatable, + IComparable, + ISpanFormattable, + IParsable, + IUtf8SpanParsable { [NonSerialized] private int hashCode; @@ -171,20 +176,88 @@ internal byte[] ToUtf8String() /// String containing the SiloAddress info to be parsed. /// New SiloAddress object created from the input data. public static SiloAddress FromParsableString(string addr) + => Parse(addr, null); + + /// + /// Parses a from a string in the standard form returned from . + /// + /// String containing the info to be parsed. + /// A parsed from the input data. + public static SiloAddress Parse(string value) + => Parse(value, null); + + /// + /// Parses a from a string in the standard form returned from . + /// + /// String containing the info to be parsed. + /// An object that provides culture-specific formatting information. This parameter is ignored. + /// A parsed from the input data. + public static SiloAddress Parse(string value, IFormatProvider? provider = null) + { + ArgumentNullException.ThrowIfNull(value); + if (!TryParse(value, provider, out var result)) + { + throw new FormatException("Invalid string SiloAddress: " + value); + } + + return result; + } + + /// + /// Tries to parse a from a string in the standard form returned from . + /// + /// String containing the info to be parsed. + /// The parsed , or if parsing failed. + /// if parsing succeeded; otherwise, . + public static bool TryParse([NotNullWhen(true)] string? value, [NotNullWhen(true)] out SiloAddress? result) + => TryParse(value, null, out result); + + /// + /// Tries to parse a from a string in the standard form returned from . + /// + /// String containing the info to be parsed. + /// An object that provides culture-specific formatting information. This parameter is ignored. + /// The parsed , or if parsing failed. + /// if parsing succeeded; otherwise, . + public static bool TryParse([NotNullWhen(true)] string? value, IFormatProvider? provider, [NotNullWhen(true)] out SiloAddress? result) + { + if (value is null) + { + result = null; + return false; + } + + return TryParse(value.AsSpan(), out result); + } + + private static bool TryParse(ReadOnlySpan addr, [NotNullWhen(true)] out SiloAddress? result) { // This must be the "inverse" of ToParsableString, and must be the same across all silos in a deployment. // Basically, this should never change unless the data content of SiloAddress changes // First is the IPEndpoint; then '@'; then the generation - int atSign = addr.LastIndexOf(SEPARATOR); + var atSign = addr.LastIndexOf(SEPARATOR); + if (atSign < 0) + { + result = null; + return false; + } + // IPEndpoint is the host, then ':', then the port - int lastColon = addr.LastIndexOf(':', atSign - 1); - if (atSign < 0 || lastColon < 0) throw new FormatException("Invalid string SiloAddress: " + addr); + var endpoint = addr[..atSign]; + var lastColon = endpoint.LastIndexOf(':'); + if (lastColon < 0 + || !IPAddress.TryParse(endpoint[..lastColon], out var host) + || !int.TryParse(endpoint[(lastColon + 1)..], NumberStyles.None, CultureInfo.InvariantCulture, out var port) + || port is < IPEndPoint.MinPort or > IPEndPoint.MaxPort + || !int.TryParse(addr[(atSign + 1)..], NumberStyles.None, CultureInfo.InvariantCulture, out var gen)) + { + result = null; + return false; + } - var host = IPAddress.Parse(addr.AsSpan(0, lastColon)); - int port = int.Parse(addr.AsSpan(lastColon + 1, atSign - lastColon - 1), NumberStyles.None); - var gen = int.Parse(addr.AsSpan(atSign + 1), NumberStyles.None); - return New(host, port, gen); + result = New(host, port, gen); + return true; } /// @@ -193,18 +266,69 @@ public static SiloAddress FromParsableString(string addr) /// String containing the SiloAddress info to be parsed. /// New SiloAddress object created from the input data. public static SiloAddress FromUtf8String(ReadOnlySpan addr) + => Parse(addr, null); + + /// + /// Parses a from UTF-8 text in the standard form returned from . + /// + /// UTF-8 text containing the info to be parsed. + /// A parsed from the input data. + public static SiloAddress Parse(ReadOnlySpan utf8Text) + => Parse(utf8Text, null); + + /// + /// Parses a from UTF-8 text in the standard form returned from . + /// + /// UTF-8 text containing the info to be parsed. + /// An object that provides culture-specific formatting information. This parameter is ignored. + /// A parsed from the input data. + public static SiloAddress Parse(ReadOnlySpan utf8Text, IFormatProvider? provider = null) + { + if (!TryParse(utf8Text, provider, out var result)) + { + ThrowInvalidUtf8SiloAddress(utf8Text); + } + + return result; + } + + /// + /// Tries to parse a from UTF-8 text in the standard form returned from . + /// + /// UTF-8 text containing the info to be parsed. + /// The parsed , or if parsing failed. + /// if parsing succeeded; otherwise, . + public static bool TryParse(ReadOnlySpan utf8Text, [NotNullWhen(true)] out SiloAddress? result) + => TryParse(utf8Text, null, out result); + + /// + /// Tries to parse a from UTF-8 text in the standard form returned from . + /// + /// UTF-8 text containing the info to be parsed. + /// An object that provides culture-specific formatting information. This parameter is ignored. + /// The parsed , or if parsing failed. + /// if parsing succeeded; otherwise, . + public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, [NotNullWhen(true)] out SiloAddress? result) { // This must be the "inverse" of ToParsableString, and must be the same across all silos in a deployment. // Basically, this should never change unless the data content of SiloAddress changes // First is the IPEndpoint; then '@'; then the generation - var atSign = addr.LastIndexOf((byte)SEPARATOR); - if (atSign < 0) ThrowInvalidUtf8SiloAddress(addr); + var atSign = utf8Text.LastIndexOf((byte)SEPARATOR); + if (atSign < 0) + { + result = null; + return false; + } // IPEndpoint is the host, then ':', then the port - var endpointSlice = addr[..atSign]; + var endpointSlice = utf8Text[..atSign]; int lastColon = endpointSlice.LastIndexOf((byte)':'); - if (lastColon < 0) ThrowInvalidUtf8SiloAddress(addr); + if (lastColon < 0) + { + result = null; + return false; + } var ipSlice = endpointSlice[..lastColon]; Span buf = stackalloc char[45]; @@ -212,17 +336,29 @@ public static SiloAddress FromUtf8String(ReadOnlySpan addr) ? buf[..Encoding.UTF8.GetChars(ipSlice, buf)] : Encoding.UTF8.GetString(ipSlice).AsSpan(); if (!IPAddress.TryParse(hostString, out var host)) - ThrowInvalidUtf8SiloAddress(addr); + { + result = null; + return false; + } var portSlice = endpointSlice[(lastColon + 1)..]; - if (!Utf8Parser.TryParse(portSlice, out int port, out len) || len < portSlice.Length) - ThrowInvalidUtf8SiloAddress(addr); + if (!Utf8Parser.TryParse(portSlice, out int port, out len) + || len != portSlice.Length + || port is < IPEndPoint.MinPort or > IPEndPoint.MaxPort) + { + result = null; + return false; + } - var genSlice = addr[(atSign + 1)..]; - if (!Utf8Parser.TryParse(genSlice, out int generation, out len) || len < genSlice.Length) - ThrowInvalidUtf8SiloAddress(addr); + var genSlice = utf8Text[(atSign + 1)..]; + if (!Utf8Parser.TryParse(genSlice, out int generation, out len) || len != genSlice.Length) + { + result = null; + return false; + } - return New(host, port, generation); + result = New(host, port, generation); + return true; } [DoesNotReturn] diff --git a/src/api/Orleans.Core.Abstractions/Orleans.Core.Abstractions.cs b/src/api/Orleans.Core.Abstractions/Orleans.Core.Abstractions.cs index 4d35bfb0d8e..8b287c36e7a 100644 --- a/src/api/Orleans.Core.Abstractions/Orleans.Core.Abstractions.cs +++ b/src/api/Orleans.Core.Abstractions/Orleans.Core.Abstractions.cs @@ -2932,7 +2932,7 @@ public sealed partial class ResourceOptimizedPlacement : PlacementStrategy [System.Text.Json.Serialization.JsonConverter(typeof(SiloAddressConverter))] [System.Diagnostics.DebuggerDisplay("SiloAddress {ToString()}")] [SuppressReferenceTracking] - public sealed partial class SiloAddress : System.IEquatable, System.IComparable, System.ISpanFormattable, System.IFormattable + public sealed partial class SiloAddress : System.IEquatable, System.IComparable, System.ISpanFormattable, System.IFormattable, System.IParsable, System.IUtf8SpanParsable { internal SiloAddress() { } @@ -2972,6 +2972,14 @@ internal SiloAddress() { } public static SiloAddress New(System.Net.IPEndPoint ep, int gen) { throw null; } + public static SiloAddress Parse(string value) { throw null; } + + public static SiloAddress Parse(string value, System.IFormatProvider? provider = null) { throw null; } + + public static SiloAddress Parse(System.ReadOnlySpan utf8Text) { throw null; } + + public static SiloAddress Parse(System.ReadOnlySpan utf8Text, System.IFormatProvider? provider = null) { throw null; } + string System.IFormattable.ToString(string? format, System.IFormatProvider? formatProvider) { throw null; } bool System.ISpanFormattable.TryFormat(System.Span destination, out int charsWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) { throw null; } @@ -2981,6 +2989,14 @@ internal SiloAddress() { } public override string ToString() { throw null; } public string ToStringWithHashCode() { throw null; } + + public static bool TryParse(string? value, out SiloAddress? result) { throw null; } + + public static bool TryParse(string? value, System.IFormatProvider? provider, out SiloAddress? result) { throw null; } + + public static bool TryParse(System.ReadOnlySpan utf8Text, out SiloAddress? result) { throw null; } + + public static bool TryParse(System.ReadOnlySpan utf8Text, System.IFormatProvider? provider, out SiloAddress? result) { throw null; } } public sealed partial class SiloAddressConverter : System.Text.Json.Serialization.JsonConverter diff --git a/test/Orleans.Core.Tests/General/Identifiertests.cs b/test/Orleans.Core.Tests/General/Identifiertests.cs index f01e1137385..ad2b3f8b927 100644 --- a/test/Orleans.Core.Tests/General/Identifiertests.cs +++ b/test/Orleans.Core.Tests/General/Identifiertests.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Text; using System.Text.Json; using Microsoft.Extensions.DependencyInjection; using Orleans; @@ -341,6 +342,33 @@ public void SiloAddress_ToFrom_ParsableString() Assert.Equal(addressStr2, addressStr2Out); // SiloAddress equal after From-To-ParsableString } + + [Fact, TestCategory("BVT"), TestCategory("Identifiers")] + public void SiloAddress_ParseInterfaces() + { + const string addressString = "127.0.0.1:11111@144611139"; + var expected = SiloAddress.FromParsableString(addressString); + + Assert.Same(expected, SiloAddress.Parse(addressString)); + Assert.Same(expected, SiloAddress.Parse(addressString, null)); + Assert.True(SiloAddress.TryParse(addressString, out var parsedWithoutProvider)); + Assert.Same(expected, parsedWithoutProvider); + Assert.True(SiloAddress.TryParse(addressString, null, out var parsed)); + Assert.Same(expected, parsed); + + var utf8 = Encoding.UTF8.GetBytes(addressString); + Assert.Same(expected, SiloAddress.Parse(utf8)); + Assert.Same(expected, SiloAddress.Parse(utf8, null)); + Assert.Same(expected, SiloAddress.FromUtf8String(utf8)); + Assert.True(SiloAddress.TryParse(utf8, out var utf8ParsedWithoutProvider)); + Assert.Same(expected, utf8ParsedWithoutProvider); + Assert.True(SiloAddress.TryParse(utf8, null, out var utf8Parsed)); + Assert.Same(expected, utf8Parsed); + + Assert.False(SiloAddress.TryParse("not-a-silo", out _)); + Assert.False(SiloAddress.TryParse(Encoding.UTF8.GetBytes("not-a-silo"), null, out _)); + } + /// /// Tests GrainReference creation, serialization, and round-trip operations. ///