From 81ab847a6cd9ea93bee73026750b9c556db5c6e4 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Mon, 6 May 2024 17:07:42 +0100 Subject: [PATCH 01/43] Changed IP address parsers to use generics This is preparation for the work required to enable IUtf8SpanParsable support on IPAddress and IPNetwork. Also replaced a few instances of unsafe code with spans. This uses the same style of generic constraints as the numeric parsing in corelib (although it can't access IUtfChar.) All tests pass, although the UTF8 code paths haven't yet been tested. Some restructuring is pending as I plumb IPAddressParser to the updated generic methods. Future commits will handle the downstream dependencies of IPvXAddressHelper - at present, this looks like Uri and TargetHostNameHelper. --- .../System/Net/IPv4AddressHelper.Common.cs | 124 +++--- .../System/Net/IPv6AddressHelper.Common.cs | 367 +++++++++--------- .../src/System/Net/IPAddressParser.cs | 63 ++- .../UnitTests/Fakes/IPv4AddressHelper.cs | 7 +- .../UnitTests/Fakes/IPv6AddressHelper.cs | 13 +- 5 files changed, 316 insertions(+), 258 deletions(-) diff --git a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs index f4dd4b77772349..bd34f2b66fbae4 100644 --- a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs @@ -2,32 +2,44 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers.Binary; +using System.Diagnostics; +using System.Numerics; -namespace System +namespace System.Net { internal static partial class IPv4AddressHelper { + internal static class IPv4Constants + where TChar : unmanaged, IBinaryInteger + { + // IPv4 address-specific generic constants. + public static readonly TChar ComponentSeparator = TChar.CreateTruncating('.'); + public static readonly TChar[] PrefixSeparators = [TChar.CreateTruncating('/'), TChar.CreateTruncating('\\')]; + public static readonly TChar[] UrlSeparators = [TChar.CreateTruncating(':'), TChar.CreateTruncating('?'), TChar.CreateTruncating('#')]; + public static readonly TChar[] HexadecimalPrefix = [TChar.CreateTruncating('0'), TChar.CreateTruncating('x')]; + public static readonly TChar OctalPrefix = HexadecimalPrefix[0]; + } + internal const long Invalid = -1; private const long MaxIPv4Value = uint.MaxValue; // the native parser cannot handle MaxIPv4Value, only MaxIPv4Value - 1 - private const int Octal = 8; - private const int Decimal = 10; - private const int Hex = 16; private const int NumberOfLabels = 4; // Only called from the IPv6Helper, only parse the canonical format - internal static int ParseHostNumber(ReadOnlySpan str, int start, int end) + internal static int ParseHostNumber(ReadOnlySpan str) + where TChar : unmanaged, IBinaryInteger { Span numbers = stackalloc byte[NumberOfLabels]; + int start = 0; for (int i = 0; i < numbers.Length; ++i) { int b = 0; - char ch; + TChar ch; - for (; (start < end) && (ch = str[start]) != '.' && ch != ':'; ++start) + for (; (start < str.Length) && (ch = str[start]) != TChar.CreateTruncating('.') && ch != TChar.CreateTruncating(':'); ++start) { - b = (b * 10) + ch - '0'; + b = (b * 10) + int.CreateTruncating(ch - TChar.CreateTruncating('0')); } numbers[i] = (byte)b; @@ -79,16 +91,17 @@ internal static int ParseHostNumber(ReadOnlySpan str, int start, int end) // //Remark: MUST NOT be used unless all input indexes are verified and trusted. - internal static unsafe bool IsValid(char* name, int start, ref int end, bool allowIPv6, bool notImplicitFile, bool unknownScheme) + internal static bool IsValid(ReadOnlySpan name, ref int bytesConsumed, bool allowIPv6, bool notImplicitFile, bool unknownScheme) + where TChar : unmanaged, IBinaryInteger { // IPv6 can only have canonical IPv4 embedded. Unknown schemes will not attempt parsing of non-canonical IPv4 addresses. if (allowIPv6 || unknownScheme) { - return IsValidCanonical(name, start, ref end, allowIPv6, notImplicitFile); + return IsValidCanonical(name, ref bytesConsumed, allowIPv6, notImplicitFile); } else { - return ParseNonCanonical(name, start, ref end, notImplicitFile) != Invalid; + return ParseNonCanonical(name, ref bytesConsumed, notImplicitFile) != Invalid; } } @@ -105,32 +118,36 @@ internal static unsafe bool IsValid(char* name, int start, ref int end, bool all // / "2" %x30-34 DIGIT ; 200-249 // / "25" %x30-35 ; 250-255 // - internal static unsafe bool IsValidCanonical(char* name, int start, ref int end, bool allowIPv6, bool notImplicitFile) + internal static bool IsValidCanonical(ReadOnlySpan name, ref int bytesConsumed, bool allowIPv6, bool notImplicitFile) + where TChar : unmanaged, IBinaryInteger { int dots = 0; int number = 0; bool haveNumber = false; bool firstCharIsZero = false; + int current; - while (start < end) + Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); + for (current = 0; current < name.Length; current++) { - char ch = name[start]; + TChar ch = name[current]; + if (allowIPv6) { // for ipv4 inside ipv6 the terminator is either ScopeId, prefix or ipv6 terminator - if (ch == ']' || ch == '/' || ch == '%') + if (ch == IPv6AddressHelper.IPv6Constants.AddressEndCharacter || ch == IPv6AddressHelper.IPv6Constants.PrefixSeparator || ch == IPv6AddressHelper.IPv6Constants.ScopeSeparator) break; } - else if (ch == '/' || ch == '\\' || (notImplicitFile && (ch == ':' || ch == '?' || ch == '#'))) + else if (Array.IndexOf(IPv4Constants.PrefixSeparators, ch) != -1 || (notImplicitFile && (Array.IndexOf(IPv4Constants.UrlSeparators, ch) != -1))) { break; } - if (char.IsAsciiDigit(ch)) + if (IPAddressParser.GenericHelpers.TryParseInteger(IPAddressParser.GenericHelpers.Decimal, ch, out int parsedCharacter)) { - if (!haveNumber && (ch == '0')) + if (!haveNumber && ch == IPv4Constants.OctalPrefix) { - if ((start + 1 < end) && name[start + 1] == '0') + if (current + 1 < name.Length && name[current + 1] == IPv4Constants.OctalPrefix) { // 00 is not allowed as a prefix. return false; @@ -140,13 +157,13 @@ internal static unsafe bool IsValidCanonical(char* name, int start, ref int end, } haveNumber = true; - number = number * 10 + (name[start] - '0'); - if (number > 255) + number = number * IPAddressParser.GenericHelpers.Decimal + parsedCharacter; + if (number > byte.MaxValue) { return false; } } - else if (ch == '.') + else if (ch == IPv4Constants.ComponentSeparator) { if (!haveNumber || (number > 0 && firstCharIsZero)) { @@ -162,12 +179,11 @@ internal static unsafe bool IsValidCanonical(char* name, int start, ref int end, { return false; } - ++start; } bool res = (dots == 3) && haveNumber; if (res) { - end = start; + bytesConsumed = current; } return res; } @@ -176,35 +192,37 @@ internal static unsafe bool IsValidCanonical(char* name, int start, ref int end, // Return Invalid (-1) for failures. // If the address has less than three dots, only the rightmost section is assumed to contain the combined value for // the missing sections: 0xFF00FFFF == 0xFF.0x00.0xFF.0xFF == 0xFF.0xFFFF - internal static unsafe long ParseNonCanonical(char* name, int start, ref int end, bool notImplicitFile) + internal static long ParseNonCanonical(ReadOnlySpan name, ref int bytesConsumed, bool notImplicitFile) + where TChar : unmanaged, IBinaryInteger { - int numberBase = Decimal; - char ch; - long* parts = stackalloc long[4]; + int numberBase = IPAddressParser.GenericHelpers.Decimal; + Span parts = stackalloc long[4]; long currentValue = 0; bool atLeastOneChar = false; // Parse one dotted section at a time int dotCount = 0; // Limit 3 - int current = start; - for (; current < end; current++) + int current; + + for (current = 0; current < name.Length; current++) { - ch = name[current]; + TChar ch = name[current]; currentValue = 0; // Figure out what base this section is in - numberBase = Decimal; - if (ch == '0') + numberBase = IPAddressParser.GenericHelpers.Decimal; + if (ch == IPv4Constants.OctalPrefix) { - numberBase = Octal; + numberBase = IPAddressParser.GenericHelpers.Octal; current++; atLeastOneChar = true; - if (current < end) + if (current < name.Length) { - ch = name[current]; - if (ch == 'x' || ch == 'X') + ch = name[current] | TChar.CreateTruncating(0x20); + + if (ch == IPv4Constants.HexadecimalPrefix[1]) { - numberBase = Hex; + numberBase = IPAddressParser.GenericHelpers.Hex; current++; atLeastOneChar = false; } @@ -212,28 +230,11 @@ internal static unsafe long ParseNonCanonical(char* name, int start, ref int end } // Parse this section - for (; current < end; current++) + for (; current < name.Length; current++) { ch = name[current]; - int digitValue; - if ((numberBase == Decimal || numberBase == Hex) && char.IsAsciiDigit(ch)) - { - digitValue = ch - '0'; - } - else if (numberBase == Octal && '0' <= ch && ch <= '7') - { - digitValue = ch - '0'; - } - else if (numberBase == Hex && 'a' <= ch && ch <= 'f') - { - digitValue = ch + 10 - 'a'; - } - else if (numberBase == Hex && 'A' <= ch && ch <= 'F') - { - digitValue = ch + 10 - 'A'; - } - else + if (!IPAddressParser.GenericHelpers.TryParseInteger(numberBase, ch, out int digitValue)) { break; // Invalid/terminator } @@ -248,7 +249,7 @@ internal static unsafe long ParseNonCanonical(char* name, int start, ref int end atLeastOneChar = true; } - if (current < end && name[current] == '.') + if (current < name.Length && name[current] == IPv4Constants.ComponentSeparator) { if (dotCount >= 3 // Max of 3 dots and 4 segments || !atLeastOneChar // No empty segmets: 1...1 @@ -271,13 +272,14 @@ internal static unsafe long ParseNonCanonical(char* name, int start, ref int end { return Invalid; // Empty trailing segment: 1.1.1. } - else if (current >= end) + else if (current >= name.Length) { // end of string, allowed } - else if ((ch = name[current]) == '/' || ch == '\\' || (notImplicitFile && (ch == ':' || ch == '?' || ch == '#'))) + else if ((Array.IndexOf(IPv4Constants.PrefixSeparators, name[current]) != -1) + || (notImplicitFile && (Array.IndexOf(IPv4Constants.UrlSeparators, name[current]) != -1))) { - end = current; + bytesConsumed = current; } else { diff --git a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs index 71c655a52efd67..98878844da2f1e 100644 --- a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs @@ -2,11 +2,26 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Numerics; -namespace System +namespace System.Net { internal static partial class IPv6AddressHelper { + internal static class IPv6Constants + where TChar : unmanaged, IBinaryInteger + { + // IPv6 address-specific generic constants. + public static readonly TChar AddressStartCharacter = TChar.CreateTruncating('['); + public static readonly TChar AddressEndCharacter = TChar.CreateTruncating(']'); + public static readonly TChar ComponentSeparator = TChar.CreateTruncating(':'); + public static readonly TChar ScopeSeparator = TChar.CreateTruncating('%'); + public static readonly TChar PrefixSeparator = TChar.CreateTruncating('/'); + public static readonly TChar PortSeparator = TChar.CreateTruncating(':'); + public static readonly TChar[] HexadecimalPrefix = [TChar.CreateTruncating('0'), TChar.CreateTruncating('x')]; + public static readonly TChar[] Compressor = [ComponentSeparator, ComponentSeparator]; + } + private const int NumberOfLabels = 8; // RFC 5952 Section 4.2.3 @@ -93,17 +108,23 @@ internal static bool ShouldHaveIpv4Embedded(ReadOnlySpan numbers) // Remarks: MUST NOT be used unless all input indexes are verified and trusted. // start must be next to '[' position, or error is reported - internal static unsafe bool IsValidStrict(char* name, int start, ref int end) + internal static bool IsValidStrict(ReadOnlySpan name) + where TChar : unmanaged, IBinaryInteger { + // Number of components in this IPv6 address int sequenceCount = 0; + // Length of the component currently being constructed int sequenceLength = 0; bool haveCompressor = false; bool haveIPv4Address = false; bool expectingNumber = true; + // Start position of the previous component int lastSequence = 1; bool needsClosingBracket = false; - if (start < end && name[start] == '[') + int start = 0; + + if (start < name.Length && name[start] == IPv6Constants.AddressStartCharacter) { start++; needsClosingBracket = true; @@ -111,19 +132,19 @@ internal static unsafe bool IsValidStrict(char* name, int start, ref int end) // IsValidStrict() is only called if there is a ':' in the name string, i.e. // it is a possible IPv6 address. So, if the string starts with a '[' and // the pointer is advanced here there are still more characters to parse. - Debug.Assert(start < end); + Debug.Assert(start < name.Length); } // Starting with a colon character is only valid if another colon follows. - if (name[start] == ':' && (start + 1 >= end || name[start + 1] != ':')) + if (name[start] == IPv6Constants.Compressor[0] && (start + 1 >= name.Length || name[start + 1] != IPv6Constants.Compressor[1])) { return false; } int i; - for (i = start; i < end; ++i) + for (i = start; i < name.Length; ++i) { - if (char.IsAsciiHexDigit(name[i])) + if (IPAddressParser.GenericHelpers.IsValidInteger(IPAddressParser.GenericHelpers.Hex, name[i])) { ++sequenceLength; expectingNumber = false; @@ -140,103 +161,114 @@ internal static unsafe bool IsValidStrict(char* name, int start, ref int end) lastSequence = i - sequenceLength; sequenceLength = 0; } - switch (name[i]) + + if (name[i] == IPv6Constants.ScopeSeparator) { - case '%': - while (i + 1 < end) - { - i++; - if (name[i] == ']') - { - goto case ']'; - } - else if (name[i] == '/') - { - goto case '/'; - } - } - break; - case ']': - if (!needsClosingBracket) - { - return false; - } - needsClosingBracket = false; + bool moveToNextCharacter = true; - // If there's more after the closing bracket, it must be a port. - // We don't use the port, but we still validate it. - if (i + 1 < end && name[i + 1] != ':') - { - return false; - } + while (i + 1 < name.Length) + { + i++; - // If there is a port, it must either be a hexadecimal or decimal number. - if (i + 3 < end && name[i + 2] == '0' && name[i + 3] == 'x') + if (name[i] == IPv6Constants.AddressEndCharacter + || name[i] == IPv6Constants.PrefixSeparator) { - i += 4; - for (; i < end; i++) - { - if (!char.IsAsciiHexDigit(name[i])) - { - return false; - } - } - } - else - { - i += 2; - for (; i < end; i++) - { - if (!char.IsAsciiDigit(name[i])) - { - return false; - } - } + moveToNextCharacter = false; + break; } + } + + if (moveToNextCharacter) + { continue; - case ':': - if ((i > 0) && (name[i - 1] == ':')) - { - if (haveCompressor) - { - // can only have one per IPv6 address - return false; - } - haveCompressor = true; - expectingNumber = false; - } - else - { - expectingNumber = true; - } - break; + } + } - case '/': + if (name[i] == IPv6Constants.AddressEndCharacter) + { + if (!needsClosingBracket) + { return false; + } + needsClosingBracket = false; + + // If there's more after the closing bracket, it must be a port. + // We don't use the port, but we still validate it. + if (i + 1 < name.Length && name[i + 1] != IPv6Constants.PortSeparator) + { + return false; + } + + int numericBase = IPAddressParser.GenericHelpers.Decimal; + + // Skip past the closing bracket and the port separator. + i += 2; + // If there is a port, it must either be a hexadecimal or decimal number. + if (i + 1 < name.Length && name.Slice(i).StartsWith(IPv6Constants.HexadecimalPrefix.AsSpan())) + { + i += IPv6Constants.HexadecimalPrefix.Length; - case '.': - if (haveIPv4Address) + numericBase = IPAddressParser.GenericHelpers.Hex; + } + + for (; i < name.Length; i++) + { + if (!IPAddressParser.GenericHelpers.IsValidInteger(numericBase, name[i])) { return false; } - - i = end; - if (!IPv4AddressHelper.IsValid(name, lastSequence, ref i, true, false, false)) + } + continue; + } + else if (name[i] == IPv6Constants.PrefixSeparator) + { + return false; + } + else if (name[i] == IPv6Constants.ComponentSeparator) + { + if (i > 0 && name.Slice(i - 1, 2).SequenceEqual(IPv6Constants.Compressor.AsSpan())) + { + if (haveCompressor) { + // can only have one per IPv6 address return false; } - // ipv4 address takes 2 slots in ipv6 address, one was just counted meeting the '.' - ++sequenceCount; - lastSequence = i - sequenceLength; - sequenceLength = 0; - haveIPv4Address = true; - --i; // it will be incremented back on the next loop - break; + haveCompressor = true; + expectingNumber = false; + } + else + { + expectingNumber = true; + } + + sequenceLength = 0; + continue; + } + else if (name[i] == IPv4AddressHelper.IPv4Constants.ComponentSeparator) + { + int ipv4AddressLength = i; + + if (haveIPv4Address) + { + return false; + } - default: + if (!IPv4AddressHelper.IsValid(name.Slice(lastSequence), ref ipv4AddressLength, true, false, false)) + { return false; + } + i = lastSequence + ipv4AddressLength; + // ipv4 address takes 2 slots in ipv6 address, one was just counted meeting the '.' + ++sequenceCount; + lastSequence = i - sequenceLength; + sequenceLength = 0; + haveIPv4Address = true; + --i; // it will be incremented back on the next loop + + continue; } - sequenceLength = 0; + + return false; } } @@ -284,113 +316,100 @@ internal static unsafe bool IsValidStrict(char* name, int start, ref int end) // Nothing // - internal static void Parse(ReadOnlySpan address, Span numbers, int start, ref string? scopeId) + internal static void Parse(ReadOnlySpan address, Span numbers, out ReadOnlySpan scopeId) + where TChar : unmanaged, IBinaryInteger { int number = 0; int index = 0; int compressorIndex = -1; bool numberIsValid = true; - //This used to be a class instance member but have not been used so far - int PrefixLength = 0; - if (address[start] == '[') + scopeId = ReadOnlySpan.Empty; + for (int i = (address[0] == IPv6Constants.AddressStartCharacter ? 1 : 0); i < address.Length && address[i] != IPv6Constants.AddressEndCharacter;) { - ++start; - } - - for (int i = start; i < address.Length && address[i] != ']';) - { - switch (address[i]) + if (address[i] == IPv6Constants.ScopeSeparator + || address[i] == IPv6Constants.PrefixSeparator) { - case '%': - if (numberIsValid) - { - numbers[index++] = (ushort)number; - numberIsValid = false; - } + if (numberIsValid) + { + numbers[index++] = (ushort)number; + numberIsValid = false; + } - start = i; - for (++i; i < address.Length && address[i] != ']' && address[i] != '/'; ++i) - { - } - scopeId = new string(address.Slice(start, i - start)); - // ignore prefix if any - for (; i < address.Length && address[i] != ']'; ++i) - { - } - break; + if (address[i] == IPv6Constants.ScopeSeparator) + { + int scopeStart = i; - case ':': - numbers[index++] = (ushort)number; - number = 0; - ++i; - if (address[i] == ':') + for (++i; i < address.Length && address[i] != IPv6Constants.AddressEndCharacter && address[i] != IPv6Constants.PrefixSeparator; ++i) { - compressorIndex = index; - ++i; - } - else if ((compressorIndex < 0) && (index < 6)) - { - // no point checking for IPv4 address if we don't - // have a compressor or we haven't seen 6 16-bit - // numbers yet - break; } + scopeId = address.Slice(scopeStart, i - scopeStart); + } + // ignore prefix if any + for (; i < address.Length && address[i] != IPv6Constants.AddressEndCharacter; ++i) + { + } + } + else if (address[i] == IPv6Constants.ComponentSeparator) + { + numbers[index++] = (ushort)number; + number = 0; + ++i; + if (address[i] == IPv6Constants.Compressor[0]) + { + compressorIndex = index; + ++i; + } + else if ((compressorIndex < 0) && (index < 6)) + { + // no point checking for IPv4 address if we don't + // have a compressor or we haven't seen 6 16-bit + // numbers yet + continue; + } - // check to see if the upcoming number is really an IPv4 - // address. If it is, convert it to 2 ushort numbers - for (int j = i; j < address.Length && - (address[j] != ']') && - (address[j] != ':') && - (address[j] != '%') && - (address[j] != '/') && - (j < i + 4); ++j) - { + // check to see if the upcoming number is really an IPv4 + // address. If it is, convert it to 2 ushort numbers + for (int j = i; j < address.Length && + (address[j] != IPv6Constants.AddressEndCharacter) && + (address[j] != IPv6Constants.ComponentSeparator) && + (address[j] != IPv6Constants.ScopeSeparator) && + (address[j] != IPv6Constants.PrefixSeparator) && + (j < i + 4); ++j) + { - if (address[j] == '.') + if (address[j] == IPv4AddressHelper.IPv4Constants.ComponentSeparator) + { + // we have an IPv4 address. Find the end of it: + // we know that since we have a valid IPv6 + // address, the only things that will terminate + // the IPv4 address are the prefix delimiter '/' + // or the end-of-string (which we conveniently + // delimited with ']') + while (j < address.Length && (address[j] != IPv6Constants.AddressEndCharacter) && (address[j] != IPv6Constants.PrefixSeparator) && (address[j] != IPv6Constants.ScopeSeparator)) { - // we have an IPv4 address. Find the end of it: - // we know that since we have a valid IPv6 - // address, the only things that will terminate - // the IPv4 address are the prefix delimiter '/' - // or the end-of-string (which we conveniently - // delimited with ']') - while (j < address.Length && (address[j] != ']') && (address[j] != '/') && (address[j] != '%')) - { - ++j; - } - number = IPv4AddressHelper.ParseHostNumber(address, i, j); - numbers[index++] = (ushort)(number >> 16); - numbers[index++] = (ushort)number; - i = j; - - // set this to avoid adding another number to - // the array if there's a prefix - number = 0; - numberIsValid = false; - break; + ++j; } - } - break; - - case '/': - if (numberIsValid) - { + number = IPv4AddressHelper.ParseHostNumber(address.Slice(i, j - i)); + numbers[index++] = (ushort)(number >> 16); numbers[index++] = (ushort)number; - numberIsValid = false; - } + i = j; - // since we have a valid IPv6 address string, the prefix - // length is the last token in the string - for (++i; address[i] != ']'; ++i) - { - PrefixLength = PrefixLength * 10 + (address[i] - '0'); + // set this to avoid adding another number to + // the array if there's a prefix + number = 0; + numberIsValid = false; + break; } - break; - - default: - number = number * 16 + Uri.FromHex(address[i++]); - break; + } + } + else if (IPAddressParser.GenericHelpers.TryParseInteger(IPAddressParser.GenericHelpers.Hex, address[i++], out int digit)) + { + number = number * IPAddressParser.GenericHelpers.Hex + digit; + } + else + { + throw new ArgumentException(null, nameof(digit)); } } diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs index afe80e3cd5b829..66c5b621b9e2dd 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs @@ -13,6 +13,42 @@ namespace System.Net { internal static class IPAddressParser { + internal static class GenericHelpers + where TChar : unmanaged, IBinaryInteger + { + public const int Octal = 8; + public const int Decimal = 10; + public const int Hex = 16; + + // Generic constants which are used for trying to parse a single digit as an integer. + private static readonly TChar NumericRangeStartCharacter = TChar.CreateTruncating('0'); + + public static bool IsValidInteger(int numericBase, TChar ch) + { + Debug.Assert(numericBase is Octal or Decimal or Hex); + + return numericBase switch + { + > 0 and < 10 => ch >= NumericRangeStartCharacter && ch - NumericRangeStartCharacter < TChar.CreateTruncating(numericBase), + Hex => HexConverter.IsHexChar(int.CreateTruncating(ch)), + _ => false + }; + } + + public static bool TryParseInteger(int numericBase, TChar ch, out int parsedNumber) + { + bool validNumber = IsValidInteger(numericBase, ch); + + // HexConverter allows digits 1-F to be mapped to integers. The octal/decimal digit range restrictions are performed + // in IsValidInteger. + parsedNumber = validNumber + ? HexConverter.FromChar(int.CreateTruncating(ch)) + : -1; + + return validNumber; + } + } + internal const int MaxIPv4StringLength = 15; // 4 numbers separated by 3 periods, with up to 3 digits per number internal const int MaxIPv6StringLength = 65; @@ -42,15 +78,12 @@ internal static class IPAddressParser throw new FormatException(SR.dns_bad_ip_address, new SocketException(SocketError.InvalidArgument)); } - private static unsafe bool TryParseIpv4(ReadOnlySpan ipSpan, out long address) + private static bool TryParseIpv4(ReadOnlySpan ipSpan, out long address) { int end = ipSpan.Length; long tmpAddr; - fixed (char* ipStringPtr = &MemoryMarshal.GetReference(ipSpan)) - { - tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipStringPtr, 0, ref end, notImplicitFile: true); - } + tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipSpan, ref end, notImplicitFile: true); if (tmpAddr != IPv4AddressHelper.Invalid && end == ipSpan.Length) { @@ -65,30 +98,24 @@ private static unsafe bool TryParseIpv4(ReadOnlySpan ipSpan, out long addr return false; } - private static unsafe bool TryParseIPv6(ReadOnlySpan ipSpan, Span numbers, int numbersLength, out uint scope) + private static bool TryParseIPv6(ReadOnlySpan ipSpan, Span numbers, int numbersLength, out uint scope) { Debug.Assert(numbersLength >= IPAddressParserStatics.IPv6AddressShorts); - int end = ipSpan.Length; + bool isValid = IPv6AddressHelper.IsValidStrict(ipSpan); - bool isValid = false; - fixed (char* ipStringPtr = &MemoryMarshal.GetReference(ipSpan)) - { - isValid = IPv6AddressHelper.IsValidStrict(ipStringPtr, 0, ref end); - } - if (isValid || (end != ipSpan.Length)) + if (isValid) { - string? scopeId = null; - IPv6AddressHelper.Parse(ipSpan, numbers, 0, ref scopeId); + IPv6AddressHelper.Parse(ipSpan, numbers, out ReadOnlySpan scopeId); - if (scopeId?.Length > 1) + if (scopeId.Length > 1) { - if (uint.TryParse(scopeId.AsSpan(1), NumberStyles.None, CultureInfo.InvariantCulture, out scope)) + if (uint.TryParse(scopeId.Slice(1), NumberStyles.None, CultureInfo.InvariantCulture, out scope)) { return true; // scopeId is a numeric value } - uint interfaceIndex = InterfaceInfoPal.InterfaceNameToIndex(scopeId); + uint interfaceIndex = InterfaceInfoPal.InterfaceNameToIndex(new string(scopeId)); if (interfaceIndex > 0) { scope = interfaceIndex; diff --git a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs index b3da3a214ad7f2..55fbbd84063f1e 100644 --- a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs +++ b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs @@ -1,11 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace System +using System.Numerics; + +namespace System.Net { internal static class IPv4AddressHelper { internal const int Invalid = -1; - internal static unsafe long ParseNonCanonical(char* name, int start, ref int end, bool notImplicitFile) => 0; + internal static unsafe long ParseNonCanonical(ReadOnlySpan name, ref int bytesConsumed, bool notImplicitFile) + where TChar : unmanaged, IBinaryInteger => 0; } } diff --git a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs index 57b2c0a760eaee..a0606a3f84a186 100644 --- a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs +++ b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs @@ -2,15 +2,22 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Numerics; -namespace System +namespace System.Net { internal static class IPv6AddressHelper { internal static unsafe (int longestSequenceStart, int longestSequenceLength) FindCompressionRange( ReadOnlySpan numbers) => (-1, -1); internal static unsafe bool ShouldHaveIpv4Embedded(ReadOnlySpan numbers) => false; - internal static unsafe bool IsValidStrict(char* name, int start, ref int end) => false; - internal static unsafe bool Parse(ReadOnlySpan ipSpan, Span numbers, int start, ref string scopeId) => false; + internal static unsafe bool IsValidStrict(ReadOnlySpan name) + where TChar : unmanaged, IBinaryInteger => false; + internal static unsafe bool Parse(ReadOnlySpan address, Span numbers, out ReadOnlySpan scopeId) + where TChar : unmanaged, IBinaryInteger + { + scopeId = ReadOnlySpan.Empty; + return false; + } } } From 2bf97ebb460689c7a7379a8b0d628418d9abcf17 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Mon, 6 May 2024 19:03:29 +0100 Subject: [PATCH 02/43] Moved generic type definitions to parser classes This allows me to eliminate the nested class and now-redundant generic type constraints on each method --- .../System/Net/IPv4AddressHelper.Common.cs | 65 +++++------ .../System/Net/IPv6AddressHelper.Common.cs | 101 +++++++++--------- .../src/System/Net/IPAddress.cs | 30 +++--- .../src/System/Net/IPAddressParser.cs | 75 +++++++------ .../UnitTests/Fakes/IPv4AddressHelper.cs | 6 +- .../UnitTests/Fakes/IPv6AddressHelper.cs | 9 +- 6 files changed, 135 insertions(+), 151 deletions(-) diff --git a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs index bd34f2b66fbae4..1a221d28ce249f 100644 --- a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs @@ -7,18 +7,15 @@ namespace System.Net { - internal static partial class IPv4AddressHelper - { - internal static class IPv4Constants + internal static partial class IPv4AddressHelper where TChar : unmanaged, IBinaryInteger - { - // IPv4 address-specific generic constants. - public static readonly TChar ComponentSeparator = TChar.CreateTruncating('.'); - public static readonly TChar[] PrefixSeparators = [TChar.CreateTruncating('/'), TChar.CreateTruncating('\\')]; - public static readonly TChar[] UrlSeparators = [TChar.CreateTruncating(':'), TChar.CreateTruncating('?'), TChar.CreateTruncating('#')]; - public static readonly TChar[] HexadecimalPrefix = [TChar.CreateTruncating('0'), TChar.CreateTruncating('x')]; - public static readonly TChar OctalPrefix = HexadecimalPrefix[0]; - } + { + // IPv4 address-specific generic constants. + public static readonly TChar ComponentSeparator = TChar.CreateTruncating('.'); + public static readonly TChar[] PrefixSeparators = [TChar.CreateTruncating('/'), TChar.CreateTruncating('\\')]; + public static readonly TChar[] UrlSeparators = [TChar.CreateTruncating(':'), TChar.CreateTruncating('?'), TChar.CreateTruncating('#')]; + public static readonly TChar[] HexadecimalPrefix = [TChar.CreateTruncating('0'), TChar.CreateTruncating('x')]; + public static readonly TChar OctalPrefix = HexadecimalPrefix[0]; internal const long Invalid = -1; private const long MaxIPv4Value = uint.MaxValue; // the native parser cannot handle MaxIPv4Value, only MaxIPv4Value - 1 @@ -26,8 +23,7 @@ internal static class IPv4Constants private const int NumberOfLabels = 4; // Only called from the IPv6Helper, only parse the canonical format - internal static int ParseHostNumber(ReadOnlySpan str) - where TChar : unmanaged, IBinaryInteger + internal static int ParseHostNumber(ReadOnlySpan str) { Span numbers = stackalloc byte[NumberOfLabels]; int start = 0; @@ -91,8 +87,7 @@ internal static int ParseHostNumber(ReadOnlySpan str) // //Remark: MUST NOT be used unless all input indexes are verified and trusted. - internal static bool IsValid(ReadOnlySpan name, ref int bytesConsumed, bool allowIPv6, bool notImplicitFile, bool unknownScheme) - where TChar : unmanaged, IBinaryInteger + internal static bool IsValid(ReadOnlySpan name, ref int bytesConsumed, bool allowIPv6, bool notImplicitFile, bool unknownScheme) { // IPv6 can only have canonical IPv4 embedded. Unknown schemes will not attempt parsing of non-canonical IPv4 addresses. if (allowIPv6 || unknownScheme) @@ -118,8 +113,7 @@ internal static bool IsValid(ReadOnlySpan name, ref int bytesConsu // / "2" %x30-34 DIGIT ; 200-249 // / "25" %x30-35 ; 250-255 // - internal static bool IsValidCanonical(ReadOnlySpan name, ref int bytesConsumed, bool allowIPv6, bool notImplicitFile) - where TChar : unmanaged, IBinaryInteger + internal static bool IsValidCanonical(ReadOnlySpan name, ref int bytesConsumed, bool allowIPv6, bool notImplicitFile) { int dots = 0; int number = 0; @@ -135,19 +129,19 @@ internal static bool IsValidCanonical(ReadOnlySpan name, ref int b if (allowIPv6) { // for ipv4 inside ipv6 the terminator is either ScopeId, prefix or ipv6 terminator - if (ch == IPv6AddressHelper.IPv6Constants.AddressEndCharacter || ch == IPv6AddressHelper.IPv6Constants.PrefixSeparator || ch == IPv6AddressHelper.IPv6Constants.ScopeSeparator) + if (ch == IPv6AddressHelper.AddressEndCharacter || ch == IPv6AddressHelper.PrefixSeparator || ch == IPv6AddressHelper.ScopeSeparator) break; } - else if (Array.IndexOf(IPv4Constants.PrefixSeparators, ch) != -1 || (notImplicitFile && (Array.IndexOf(IPv4Constants.UrlSeparators, ch) != -1))) + else if (Array.IndexOf(PrefixSeparators, ch) != -1 || (notImplicitFile && (Array.IndexOf(UrlSeparators, ch) != -1))) { break; } - if (IPAddressParser.GenericHelpers.TryParseInteger(IPAddressParser.GenericHelpers.Decimal, ch, out int parsedCharacter)) + if (IPAddressParser.TryParseInteger(IPAddressParser.Decimal, ch, out int parsedCharacter)) { - if (!haveNumber && ch == IPv4Constants.OctalPrefix) + if (!haveNumber && ch == OctalPrefix) { - if (current + 1 < name.Length && name[current + 1] == IPv4Constants.OctalPrefix) + if (current + 1 < name.Length && name[current + 1] == OctalPrefix) { // 00 is not allowed as a prefix. return false; @@ -157,13 +151,13 @@ internal static bool IsValidCanonical(ReadOnlySpan name, ref int b } haveNumber = true; - number = number * IPAddressParser.GenericHelpers.Decimal + parsedCharacter; + number = number * IPAddressParser.Decimal + parsedCharacter; if (number > byte.MaxValue) { return false; } } - else if (ch == IPv4Constants.ComponentSeparator) + else if (ch == ComponentSeparator) { if (!haveNumber || (number > 0 && firstCharIsZero)) { @@ -192,10 +186,9 @@ internal static bool IsValidCanonical(ReadOnlySpan name, ref int b // Return Invalid (-1) for failures. // If the address has less than three dots, only the rightmost section is assumed to contain the combined value for // the missing sections: 0xFF00FFFF == 0xFF.0x00.0xFF.0xFF == 0xFF.0xFFFF - internal static long ParseNonCanonical(ReadOnlySpan name, ref int bytesConsumed, bool notImplicitFile) - where TChar : unmanaged, IBinaryInteger + internal static long ParseNonCanonical(ReadOnlySpan name, ref int bytesConsumed, bool notImplicitFile) { - int numberBase = IPAddressParser.GenericHelpers.Decimal; + int numberBase = IPAddressParser.Decimal; Span parts = stackalloc long[4]; long currentValue = 0; bool atLeastOneChar = false; @@ -210,19 +203,19 @@ internal static long ParseNonCanonical(ReadOnlySpan name, ref int currentValue = 0; // Figure out what base this section is in - numberBase = IPAddressParser.GenericHelpers.Decimal; - if (ch == IPv4Constants.OctalPrefix) + numberBase = IPAddressParser.Decimal; + if (ch == OctalPrefix) { - numberBase = IPAddressParser.GenericHelpers.Octal; + numberBase = IPAddressParser.Octal; current++; atLeastOneChar = true; if (current < name.Length) { ch = name[current] | TChar.CreateTruncating(0x20); - if (ch == IPv4Constants.HexadecimalPrefix[1]) + if (ch == HexadecimalPrefix[1]) { - numberBase = IPAddressParser.GenericHelpers.Hex; + numberBase = IPAddressParser.Hex; current++; atLeastOneChar = false; } @@ -234,7 +227,7 @@ internal static long ParseNonCanonical(ReadOnlySpan name, ref int { ch = name[current]; - if (!IPAddressParser.GenericHelpers.TryParseInteger(numberBase, ch, out int digitValue)) + if (!IPAddressParser.TryParseInteger(numberBase, ch, out int digitValue)) { break; // Invalid/terminator } @@ -249,7 +242,7 @@ internal static long ParseNonCanonical(ReadOnlySpan name, ref int atLeastOneChar = true; } - if (current < name.Length && name[current] == IPv4Constants.ComponentSeparator) + if (current < name.Length && name[current] == ComponentSeparator) { if (dotCount >= 3 // Max of 3 dots and 4 segments || !atLeastOneChar // No empty segmets: 1...1 @@ -276,8 +269,8 @@ internal static long ParseNonCanonical(ReadOnlySpan name, ref int { // end of string, allowed } - else if ((Array.IndexOf(IPv4Constants.PrefixSeparators, name[current]) != -1) - || (notImplicitFile && (Array.IndexOf(IPv4Constants.UrlSeparators, name[current]) != -1))) + else if ((Array.IndexOf(PrefixSeparators, name[current]) != -1) + || (notImplicitFile && (Array.IndexOf(UrlSeparators, name[current]) != -1))) { bytesConsumed = current; } diff --git a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs index 98878844da2f1e..4f50c2f505e5f9 100644 --- a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs @@ -6,21 +6,18 @@ namespace System.Net { - internal static partial class IPv6AddressHelper + internal static partial class IPv6AddressHelper + where TChar : unmanaged, IBinaryInteger { - internal static class IPv6Constants - where TChar : unmanaged, IBinaryInteger - { - // IPv6 address-specific generic constants. - public static readonly TChar AddressStartCharacter = TChar.CreateTruncating('['); - public static readonly TChar AddressEndCharacter = TChar.CreateTruncating(']'); - public static readonly TChar ComponentSeparator = TChar.CreateTruncating(':'); - public static readonly TChar ScopeSeparator = TChar.CreateTruncating('%'); - public static readonly TChar PrefixSeparator = TChar.CreateTruncating('/'); - public static readonly TChar PortSeparator = TChar.CreateTruncating(':'); - public static readonly TChar[] HexadecimalPrefix = [TChar.CreateTruncating('0'), TChar.CreateTruncating('x')]; - public static readonly TChar[] Compressor = [ComponentSeparator, ComponentSeparator]; - } + // IPv6 address-specific generic constants. + public static readonly TChar AddressStartCharacter = TChar.CreateTruncating('['); + public static readonly TChar AddressEndCharacter = TChar.CreateTruncating(']'); + public static readonly TChar ComponentSeparator = TChar.CreateTruncating(':'); + public static readonly TChar ScopeSeparator = TChar.CreateTruncating('%'); + public static readonly TChar PrefixSeparator = TChar.CreateTruncating('/'); + public static readonly TChar PortSeparator = TChar.CreateTruncating(':'); + public static readonly TChar[] HexadecimalPrefix = [TChar.CreateTruncating('0'), TChar.CreateTruncating('x')]; + public static readonly TChar[] Compressor = [ComponentSeparator, ComponentSeparator]; private const int NumberOfLabels = 8; @@ -108,8 +105,7 @@ internal static bool ShouldHaveIpv4Embedded(ReadOnlySpan numbers) // Remarks: MUST NOT be used unless all input indexes are verified and trusted. // start must be next to '[' position, or error is reported - internal static bool IsValidStrict(ReadOnlySpan name) - where TChar : unmanaged, IBinaryInteger + internal static bool IsValidStrict(ReadOnlySpan name) { // Number of components in this IPv6 address int sequenceCount = 0; @@ -124,7 +120,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) bool needsClosingBracket = false; int start = 0; - if (start < name.Length && name[start] == IPv6Constants.AddressStartCharacter) + if (start < name.Length && name[start] == AddressStartCharacter) { start++; needsClosingBracket = true; @@ -136,7 +132,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) } // Starting with a colon character is only valid if another colon follows. - if (name[start] == IPv6Constants.Compressor[0] && (start + 1 >= name.Length || name[start + 1] != IPv6Constants.Compressor[1])) + if (name[start] == Compressor[0] && (start + 1 >= name.Length || name[start + 1] != Compressor[1])) { return false; } @@ -144,7 +140,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) int i; for (i = start; i < name.Length; ++i) { - if (IPAddressParser.GenericHelpers.IsValidInteger(IPAddressParser.GenericHelpers.Hex, name[i])) + if (IPAddressParser.IsValidInteger(IPAddressParser.Hex, name[i])) { ++sequenceLength; expectingNumber = false; @@ -162,7 +158,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) sequenceLength = 0; } - if (name[i] == IPv6Constants.ScopeSeparator) + if (name[i] == ScopeSeparator) { bool moveToNextCharacter = true; @@ -170,8 +166,8 @@ internal static bool IsValidStrict(ReadOnlySpan name) { i++; - if (name[i] == IPv6Constants.AddressEndCharacter - || name[i] == IPv6Constants.PrefixSeparator) + if (name[i] == AddressEndCharacter + || name[i] == PrefixSeparator) { moveToNextCharacter = false; break; @@ -184,7 +180,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) } } - if (name[i] == IPv6Constants.AddressEndCharacter) + if (name[i] == AddressEndCharacter) { if (!needsClosingBracket) { @@ -194,39 +190,39 @@ internal static bool IsValidStrict(ReadOnlySpan name) // If there's more after the closing bracket, it must be a port. // We don't use the port, but we still validate it. - if (i + 1 < name.Length && name[i + 1] != IPv6Constants.PortSeparator) + if (i + 1 < name.Length && name[i + 1] != PortSeparator) { return false; } - int numericBase = IPAddressParser.GenericHelpers.Decimal; + int numericBase = IPAddressParser.Decimal; // Skip past the closing bracket and the port separator. i += 2; // If there is a port, it must either be a hexadecimal or decimal number. - if (i + 1 < name.Length && name.Slice(i).StartsWith(IPv6Constants.HexadecimalPrefix.AsSpan())) + if (i + 1 < name.Length && name.Slice(i).StartsWith(HexadecimalPrefix.AsSpan())) { - i += IPv6Constants.HexadecimalPrefix.Length; + i += HexadecimalPrefix.Length; - numericBase = IPAddressParser.GenericHelpers.Hex; + numericBase = IPAddressParser.Hex; } for (; i < name.Length; i++) { - if (!IPAddressParser.GenericHelpers.IsValidInteger(numericBase, name[i])) + if (!IPAddressParser.IsValidInteger(numericBase, name[i])) { return false; } } continue; } - else if (name[i] == IPv6Constants.PrefixSeparator) + else if (name[i] == PrefixSeparator) { return false; } - else if (name[i] == IPv6Constants.ComponentSeparator) + else if (name[i] == ComponentSeparator) { - if (i > 0 && name.Slice(i - 1, 2).SequenceEqual(IPv6Constants.Compressor.AsSpan())) + if (i > 0 && name.Slice(i - 1, 2).SequenceEqual(Compressor.AsSpan())) { if (haveCompressor) { @@ -244,7 +240,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) sequenceLength = 0; continue; } - else if (name[i] == IPv4AddressHelper.IPv4Constants.ComponentSeparator) + else if (name[i] == IPv4AddressHelper.ComponentSeparator) { int ipv4AddressLength = i; @@ -253,7 +249,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) return false; } - if (!IPv4AddressHelper.IsValid(name.Slice(lastSequence), ref ipv4AddressLength, true, false, false)) + if (!IPv4AddressHelper.IsValid(name.Slice(lastSequence), ref ipv4AddressLength, true, false, false)) { return false; } @@ -316,8 +312,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) // Nothing // - internal static void Parse(ReadOnlySpan address, Span numbers, out ReadOnlySpan scopeId) - where TChar : unmanaged, IBinaryInteger + internal static void Parse(ReadOnlySpan address, Span numbers, out ReadOnlySpan scopeId) { int number = 0; int index = 0; @@ -325,10 +320,10 @@ internal static void Parse(ReadOnlySpan address, Span numb bool numberIsValid = true; scopeId = ReadOnlySpan.Empty; - for (int i = (address[0] == IPv6Constants.AddressStartCharacter ? 1 : 0); i < address.Length && address[i] != IPv6Constants.AddressEndCharacter;) + for (int i = (address[0] == AddressStartCharacter ? 1 : 0); i < address.Length && address[i] != AddressEndCharacter;) { - if (address[i] == IPv6Constants.ScopeSeparator - || address[i] == IPv6Constants.PrefixSeparator) + if (address[i] == ScopeSeparator + || address[i] == PrefixSeparator) { if (numberIsValid) { @@ -336,26 +331,26 @@ internal static void Parse(ReadOnlySpan address, Span numb numberIsValid = false; } - if (address[i] == IPv6Constants.ScopeSeparator) + if (address[i] == ScopeSeparator) { int scopeStart = i; - for (++i; i < address.Length && address[i] != IPv6Constants.AddressEndCharacter && address[i] != IPv6Constants.PrefixSeparator; ++i) + for (++i; i < address.Length && address[i] != AddressEndCharacter && address[i] != PrefixSeparator; ++i) { } scopeId = address.Slice(scopeStart, i - scopeStart); } // ignore prefix if any - for (; i < address.Length && address[i] != IPv6Constants.AddressEndCharacter; ++i) + for (; i < address.Length && address[i] != AddressEndCharacter; ++i) { } } - else if (address[i] == IPv6Constants.ComponentSeparator) + else if (address[i] == ComponentSeparator) { numbers[index++] = (ushort)number; number = 0; ++i; - if (address[i] == IPv6Constants.Compressor[0]) + if (address[i] == Compressor[0]) { compressorIndex = index; ++i; @@ -371,14 +366,14 @@ internal static void Parse(ReadOnlySpan address, Span numb // check to see if the upcoming number is really an IPv4 // address. If it is, convert it to 2 ushort numbers for (int j = i; j < address.Length && - (address[j] != IPv6Constants.AddressEndCharacter) && - (address[j] != IPv6Constants.ComponentSeparator) && - (address[j] != IPv6Constants.ScopeSeparator) && - (address[j] != IPv6Constants.PrefixSeparator) && + (address[j] != AddressEndCharacter) && + (address[j] != ComponentSeparator) && + (address[j] != ScopeSeparator) && + (address[j] != PrefixSeparator) && (j < i + 4); ++j) { - if (address[j] == IPv4AddressHelper.IPv4Constants.ComponentSeparator) + if (address[j] == IPv4AddressHelper.ComponentSeparator) { // we have an IPv4 address. Find the end of it: // we know that since we have a valid IPv6 @@ -386,11 +381,11 @@ internal static void Parse(ReadOnlySpan address, Span numb // the IPv4 address are the prefix delimiter '/' // or the end-of-string (which we conveniently // delimited with ']') - while (j < address.Length && (address[j] != IPv6Constants.AddressEndCharacter) && (address[j] != IPv6Constants.PrefixSeparator) && (address[j] != IPv6Constants.ScopeSeparator)) + while (j < address.Length && (address[j] != AddressEndCharacter) && (address[j] != PrefixSeparator) && (address[j] != ScopeSeparator)) { ++j; } - number = IPv4AddressHelper.ParseHostNumber(address.Slice(i, j - i)); + number = IPv4AddressHelper.ParseHostNumber(address.Slice(i, j - i)); numbers[index++] = (ushort)(number >> 16); numbers[index++] = (ushort)number; i = j; @@ -403,9 +398,9 @@ internal static void Parse(ReadOnlySpan address, Span numb } } } - else if (IPAddressParser.GenericHelpers.TryParseInteger(IPAddressParser.GenericHelpers.Hex, address[i++], out int digit)) + else if (IPAddressParser.TryParseInteger(IPAddressParser.Hex, address[i++], out int digit)) { - number = number * IPAddressParser.GenericHelpers.Hex + digit; + number = number * IPAddressParser.Hex + digit; } else { diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs index 48f424407b400c..342dd40c4abb86 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs @@ -228,13 +228,13 @@ public static bool TryParse([NotNullWhen(true)] string? ipString, [NotNullWhen(t return false; } - address = IPAddressParser.Parse(ipString.AsSpan(), tryParse: true); + address = IPAddressParser.Parse(ipString.AsSpan(), tryParse: true); return (address != null); } public static bool TryParse(ReadOnlySpan ipSpan, [NotNullWhen(true)] out IPAddress? address) { - address = IPAddressParser.Parse(ipSpan, tryParse: true); + address = IPAddressParser.Parse(ipSpan, tryParse: true); return (address != null); } @@ -252,12 +252,12 @@ public static IPAddress Parse(string ipString) { ArgumentNullException.ThrowIfNull(ipString); - return IPAddressParser.Parse(ipString.AsSpan(), tryParse: false)!; + return IPAddressParser.Parse(ipString.AsSpan(), tryParse: false)!; } public static IPAddress Parse(ReadOnlySpan ipSpan) { - return IPAddressParser.Parse(ipSpan, tryParse: false)!; + return IPAddressParser.Parse(ipSpan, tryParse: false)!; } /// @@ -408,10 +408,10 @@ public override string ToString() string? toString = _toString; if (toString is null) { - Span span = stackalloc char[IPAddressParser.MaxIPv6StringLength]; + Span span = stackalloc char[IPAddressParser.MaxIPv6StringLength]; int length = IsIPv4 ? - IPAddressParser.FormatIPv4Address(_addressOrScopeId, span) : - IPAddressParser.FormatIPv6Address(_numbers, _addressOrScopeId, span); + IPAddressParser.FormatIPv4Address(_addressOrScopeId, span) : + IPAddressParser.FormatIPv6Address(_numbers, _addressOrScopeId, span); _toString = toString = new string(span.Slice(0, length)); } @@ -447,27 +447,27 @@ private bool TryFormatCore(Span destination, out int charsWritten) { if (IsIPv4) { - if (destination.Length >= IPAddressParser.MaxIPv4StringLength) + if (destination.Length >= IPAddressParser.MaxIPv4StringLength) { - charsWritten = IPAddressParser.FormatIPv4Address(_addressOrScopeId, destination); + charsWritten = IPAddressParser.FormatIPv4Address(_addressOrScopeId, destination); return true; } } else { - if (destination.Length >= IPAddressParser.MaxIPv6StringLength) + if (destination.Length >= IPAddressParser.MaxIPv6StringLength) { - charsWritten = IPAddressParser.FormatIPv6Address(_numbers, _addressOrScopeId, destination); + charsWritten = IPAddressParser.FormatIPv6Address(_numbers, _addressOrScopeId, destination); return true; } } - Span tmpDestination = stackalloc TChar[IPAddressParser.MaxIPv6StringLength]; - Debug.Assert(tmpDestination.Length >= IPAddressParser.MaxIPv4StringLength); + Span tmpDestination = stackalloc TChar[IPAddressParser.MaxIPv6StringLength]; + Debug.Assert(tmpDestination.Length >= IPAddressParser.MaxIPv4StringLength); int written = IsIPv4 ? - IPAddressParser.FormatIPv4Address(PrivateAddress, tmpDestination) : - IPAddressParser.FormatIPv6Address(_numbers, PrivateScopeId, tmpDestination); + IPAddressParser.FormatIPv4Address(PrivateAddress, tmpDestination) : + IPAddressParser.FormatIPv6Address(_numbers, PrivateScopeId, tmpDestination); if (tmpDestination.Slice(0, written).TryCopyTo(destination)) { diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs index 66c5b621b9e2dd..2736f57dc00753 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs @@ -11,46 +11,43 @@ namespace System.Net { - internal static class IPAddressParser + internal static class IPAddressParser + where TChar : unmanaged, IBinaryInteger { - internal static class GenericHelpers - where TChar : unmanaged, IBinaryInteger - { - public const int Octal = 8; - public const int Decimal = 10; - public const int Hex = 16; + public const int Octal = 8; + public const int Decimal = 10; + public const int Hex = 16; - // Generic constants which are used for trying to parse a single digit as an integer. - private static readonly TChar NumericRangeStartCharacter = TChar.CreateTruncating('0'); + internal const int MaxIPv4StringLength = 15; // 4 numbers separated by 3 periods, with up to 3 digits per number + internal const int MaxIPv6StringLength = 65; - public static bool IsValidInteger(int numericBase, TChar ch) - { - Debug.Assert(numericBase is Octal or Decimal or Hex); + // Generic constants which are used for trying to parse a single digit as an integer. + private static readonly TChar NumericRangeStartCharacter = TChar.CreateTruncating('0'); - return numericBase switch - { - > 0 and < 10 => ch >= NumericRangeStartCharacter && ch - NumericRangeStartCharacter < TChar.CreateTruncating(numericBase), - Hex => HexConverter.IsHexChar(int.CreateTruncating(ch)), - _ => false - }; - } + public static bool IsValidInteger(int numericBase, TChar ch) + { + Debug.Assert(numericBase is Octal or Decimal or Hex); - public static bool TryParseInteger(int numericBase, TChar ch, out int parsedNumber) + return numericBase switch { - bool validNumber = IsValidInteger(numericBase, ch); + > 0 and < 10 => ch >= NumericRangeStartCharacter && ch - NumericRangeStartCharacter < TChar.CreateTruncating(numericBase), + Hex => HexConverter.IsHexChar(int.CreateTruncating(ch)), + _ => false + }; + } - // HexConverter allows digits 1-F to be mapped to integers. The octal/decimal digit range restrictions are performed - // in IsValidInteger. - parsedNumber = validNumber - ? HexConverter.FromChar(int.CreateTruncating(ch)) - : -1; + public static bool TryParseInteger(int numericBase, TChar ch, out int parsedNumber) + { + bool validNumber = IsValidInteger(numericBase, ch); - return validNumber; - } - } + // HexConverter allows digits 1-F to be mapped to integers. The octal/decimal digit range restrictions are performed + // in IsValidInteger. + parsedNumber = validNumber + ? HexConverter.FromChar(int.CreateTruncating(ch)) + : -1; - internal const int MaxIPv4StringLength = 15; // 4 numbers separated by 3 periods, with up to 3 digits per number - internal const int MaxIPv6StringLength = 65; + return validNumber; + } internal static IPAddress? Parse(ReadOnlySpan ipSpan, bool tryParse) { @@ -83,9 +80,9 @@ private static bool TryParseIpv4(ReadOnlySpan ipSpan, out long address) int end = ipSpan.Length; long tmpAddr; - tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipSpan, ref end, notImplicitFile: true); + tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipSpan, ref end, notImplicitFile: true); - if (tmpAddr != IPv4AddressHelper.Invalid && end == ipSpan.Length) + if (tmpAddr != IPv4AddressHelper.Invalid && end == ipSpan.Length) { // IPv4AddressHelper.ParseNonCanonical returns the bytes in host order. // Convert to network order and return success. @@ -102,11 +99,11 @@ private static bool TryParseIPv6(ReadOnlySpan ipSpan, Span numbers { Debug.Assert(numbersLength >= IPAddressParserStatics.IPv6AddressShorts); - bool isValid = IPv6AddressHelper.IsValidStrict(ipSpan); + bool isValid = IPv6AddressHelper.IsValidStrict(ipSpan); if (isValid) { - IPv6AddressHelper.Parse(ipSpan, numbers, out ReadOnlySpan scopeId); + IPv6AddressHelper.Parse(ipSpan, numbers, out ReadOnlySpan scopeId); if (scopeId.Length > 1) { @@ -134,7 +131,7 @@ private static bool TryParseIPv6(ReadOnlySpan ipSpan, Span numbers return false; } - internal static int FormatIPv4Address(uint address, Span addressString) where TChar : unmanaged, IBinaryInteger + internal static int FormatIPv4Address(uint address, Span addressString) { address = (uint)IPAddress.NetworkToHostOrder(unchecked((int)address)); @@ -178,11 +175,11 @@ static int FormatByte(uint number, Span addressString) } } - internal static int FormatIPv6Address(ushort[] address, uint scopeId, Span destination) where TChar : unmanaged, IBinaryInteger + internal static int FormatIPv6Address(ushort[] address, uint scopeId, Span destination) { int pos = 0; - if (IPv6AddressHelper.ShouldHaveIpv4Embedded(address)) + if (IPv6AddressHelper.ShouldHaveIpv4Embedded(address)) { // We need to treat the last 2 ushorts as a 4-byte IPv4 address, // so output the first 6 ushorts normally, followed by the IPv4 address. @@ -228,7 +225,7 @@ internal static int FormatIPv6Address(ushort[] address, uint scopeId, Spa static void AppendSections(ReadOnlySpan address, Span destination, ref int offset) { // Find the longest sequence of zeros to be combined into a "::" - (int zeroStart, int zeroEnd) = IPv6AddressHelper.FindCompressionRange(address); + (int zeroStart, int zeroEnd) = IPv6AddressHelper.FindCompressionRange(address); bool needsColon = false; // Handle a zero sequence if there is one diff --git a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs index 55fbbd84063f1e..0e532c550cd341 100644 --- a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs +++ b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs @@ -5,10 +5,10 @@ namespace System.Net { - internal static class IPv4AddressHelper + internal static class IPv4AddressHelper + where TChar : unmanaged, IBinaryInteger { internal const int Invalid = -1; - internal static unsafe long ParseNonCanonical(ReadOnlySpan name, ref int bytesConsumed, bool notImplicitFile) - where TChar : unmanaged, IBinaryInteger => 0; + internal static unsafe long ParseNonCanonical(ReadOnlySpan name, ref int bytesConsumed, bool notImplicitFile) => 0; } } diff --git a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs index a0606a3f84a186..cd8664160d972e 100644 --- a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs +++ b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs @@ -6,15 +6,14 @@ namespace System.Net { - internal static class IPv6AddressHelper + internal static class IPv6AddressHelper + where TChar : unmanaged, IBinaryInteger { internal static unsafe (int longestSequenceStart, int longestSequenceLength) FindCompressionRange( ReadOnlySpan numbers) => (-1, -1); internal static unsafe bool ShouldHaveIpv4Embedded(ReadOnlySpan numbers) => false; - internal static unsafe bool IsValidStrict(ReadOnlySpan name) - where TChar : unmanaged, IBinaryInteger => false; - internal static unsafe bool Parse(ReadOnlySpan address, Span numbers, out ReadOnlySpan scopeId) - where TChar : unmanaged, IBinaryInteger + internal static unsafe bool IsValidStrict(ReadOnlySpan name) => false; + internal static unsafe bool Parse(ReadOnlySpan address, Span numbers, out ReadOnlySpan scopeId) { scopeId = ReadOnlySpan.Empty; return false; From 223cece8c0bceb6bfafd31b9ddef52c28c3b95c4 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Mon, 6 May 2024 21:18:32 +0100 Subject: [PATCH 03/43] Implemented IUtf8SpanParsable --- .../ref/System.Net.Primitives.cs | 6 +- .../src/System/Net/IPAddress.cs | 14 ++++- .../src/System/Net/IPAddressParser.cs | 59 ++++++++++++++----- .../UnitTests/Fakes/IPv6AddressHelper.cs | 2 + 4 files changed, 65 insertions(+), 16 deletions(-) diff --git a/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs b/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs index 480548a33f1b7d..bb1b6c06254deb 100644 --- a/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs +++ b/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs @@ -4,6 +4,8 @@ // Changes to this file must follow the https://aka.ms/api-review process. // ------------------------------------------------------------------------------ +using System.Diagnostics.CodeAnalysis; + namespace System.Net { [System.FlagsAttribute] @@ -223,7 +225,7 @@ public partial interface ICredentialsByHost { System.Net.NetworkCredential? GetCredential(string host, int port, string authenticationType); } - public partial class IPAddress : ISpanFormattable, ISpanParsable, IUtf8SpanFormattable + public partial class IPAddress : ISpanFormattable, ISpanParsable, IUtf8SpanFormattable, IUtf8SpanParsable { public static readonly System.Net.IPAddress Any; public static readonly System.Net.IPAddress Broadcast; @@ -261,6 +263,7 @@ public IPAddress(System.ReadOnlySpan address, long scopeid) { } public static long NetworkToHostOrder(long network) { throw null; } public static System.Net.IPAddress Parse(System.ReadOnlySpan ipSpan) { throw null; } public static System.Net.IPAddress Parse(string ipString) { throw null; } + public static System.Net.IPAddress Parse(ReadOnlySpan utf8Text, IFormatProvider? provider) { throw null; } static IPAddress ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) { throw null; } static IPAddress IParsable.Parse(string s, IFormatProvider? provider) { throw null; } public override string ToString() { throw null; } @@ -271,6 +274,7 @@ public IPAddress(System.ReadOnlySpan address, long scopeid) { } bool System.IUtf8SpanFormattable.TryFormat(System.Span utf8Destination, out int bytesWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) { throw null; } public static bool TryParse(System.ReadOnlySpan ipSpan, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Net.IPAddress? address) { throw null; } public static bool TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? ipString, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Net.IPAddress? address) { throw null; } + public static bool TryParse(System.ReadOnlySpan utf8Text, System.IFormatProvider? provider, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out System.Net.IPAddress? result) { throw null; } static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, out IPAddress result) { throw null; } static bool IParsable.TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, IFormatProvider? provider, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out IPAddress? result) { throw null; } public bool TryWriteBytes(System.Span destination, out int bytesWritten) { throw null; } diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs index 342dd40c4abb86..3275a80121393e 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs @@ -19,7 +19,7 @@ namespace System.Net /// Provides an Internet Protocol (IP) address. /// /// - public class IPAddress : ISpanFormattable, ISpanParsable, IUtf8SpanFormattable + public class IPAddress : ISpanFormattable, ISpanParsable, IUtf8SpanFormattable, IUtf8SpanParsable { public static readonly IPAddress Any = new ReadOnlyIPAddress([0, 0, 0, 0]); public static readonly IPAddress Loopback = new ReadOnlyIPAddress([127, 0, 0, 1]); @@ -238,6 +238,13 @@ public static bool TryParse(ReadOnlySpan ipSpan, [NotNullWhen(true)] out I return (address != null); } + /// + public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, [NotNullWhen(true)] out IPAddress? result) + { + result = IPAddressParser.Parse(utf8Text, tryParse: true); + return (result != null); + } + /// static bool IParsable.TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [NotNullWhen(true)] out IPAddress? result) => // provider is explicitly ignored @@ -260,6 +267,11 @@ public static IPAddress Parse(ReadOnlySpan ipSpan) return IPAddressParser.Parse(ipSpan, tryParse: false)!; } + /// + public static IPAddress Parse(ReadOnlySpan utf8Text, IFormatProvider? provider) => + // provider is explicitly ignored + IPAddressParser.Parse(utf8Text, tryParse: false)!; + /// static IPAddress ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => // provider is explicitly ignored diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs index 2736f57dc00753..fa70b97a7edfee 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs @@ -49,9 +49,11 @@ public static bool TryParseInteger(int numericBase, TChar ch, out int parsedNumb return validNumber; } - internal static IPAddress? Parse(ReadOnlySpan ipSpan, bool tryParse) + internal static IPAddress? Parse(ReadOnlySpan ipSpan, bool tryParse) { - if (ipSpan.Contains(':')) + Debug.Assert(typeof(TChar) == typeof(byte) || typeof(TChar) == typeof(char)); + + if (ipSpan.Contains(IPv6AddressHelper.ComponentSeparator)) { // The address is parsed as IPv6 if and only if it contains a colon. This is valid because // we don't support/parse a port specification at the end of an IPv4 address. @@ -75,14 +77,14 @@ public static bool TryParseInteger(int numericBase, TChar ch, out int parsedNumb throw new FormatException(SR.dns_bad_ip_address, new SocketException(SocketError.InvalidArgument)); } - private static bool TryParseIpv4(ReadOnlySpan ipSpan, out long address) + private static bool TryParseIpv4(ReadOnlySpan ipSpan, out long address) { int end = ipSpan.Length; long tmpAddr; - tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipSpan, ref end, notImplicitFile: true); + tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipSpan, ref end, notImplicitFile: true); - if (tmpAddr != IPv4AddressHelper.Invalid && end == ipSpan.Length) + if (tmpAddr != IPv4AddressHelper.Invalid && end == ipSpan.Length) { // IPv4AddressHelper.ParseNonCanonical returns the bytes in host order. // Convert to network order and return success. @@ -95,24 +97,55 @@ private static bool TryParseIpv4(ReadOnlySpan ipSpan, out long address) return false; } - private static bool TryParseIPv6(ReadOnlySpan ipSpan, Span numbers, int numbersLength, out uint scope) + private static bool TryParseIPv6(ReadOnlySpan ipSpan, Span numbers, int numbersLength, out uint scope) { Debug.Assert(numbersLength >= IPAddressParserStatics.IPv6AddressShorts); - bool isValid = IPv6AddressHelper.IsValidStrict(ipSpan); + bool isValid = IPv6AddressHelper.IsValidStrict(ipSpan); + scope = 0; if (isValid) { - IPv6AddressHelper.Parse(ipSpan, numbers, out ReadOnlySpan scopeId); + IPv6AddressHelper.Parse(ipSpan, numbers, out ReadOnlySpan scopeIdSpan); - if (scopeId.Length > 1) + if (scopeIdSpan.Length > 1) { - if (uint.TryParse(scopeId.Slice(1), NumberStyles.None, CultureInfo.InvariantCulture, out scope)) + string interfaceName = string.Empty; + scopeIdSpan = scopeIdSpan.Slice(1); + + // scopeId is a numeric value + if (typeof(TChar) == typeof(byte)) + { + ReadOnlySpan castScopeIdSpan = MemoryMarshal.Cast(scopeIdSpan); + + if (uint.TryParse(castScopeIdSpan, NumberStyles.None, CultureInfo.InvariantCulture, out scope)) + { + return true; + } + + interfaceName = System.Text.Encoding.UTF8.GetString(castScopeIdSpan); + } + + if (typeof(TChar) == typeof(char)) { - return true; // scopeId is a numeric value + ReadOnlySpan castScopeIdSpan = MemoryMarshal.Cast(scopeIdSpan); + + if (uint.TryParse(castScopeIdSpan, NumberStyles.None, CultureInfo.InvariantCulture, out scope)) + { + return true; + } + + interfaceName = new string(castScopeIdSpan); } - uint interfaceIndex = InterfaceInfoPal.InterfaceNameToIndex(new string(scopeId)); + // This can only happen if TChar is neither char nor byte. This is protected against by this method's + // only call site. + if (string.IsNullOrEmpty(interfaceName)) + { + return false; + } + + uint interfaceIndex = InterfaceInfoPal.InterfaceNameToIndex(interfaceName); if (interfaceIndex > 0) { scope = interfaceIndex; @@ -123,11 +156,9 @@ private static bool TryParseIPv6(ReadOnlySpan ipSpan, Span numbers } // scopeId is not presented - scope = 0; return true; } - scope = 0; return false; } diff --git a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs index cd8664160d972e..daa43df211c366 100644 --- a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs +++ b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs @@ -9,6 +9,8 @@ namespace System.Net internal static class IPv6AddressHelper where TChar : unmanaged, IBinaryInteger { + public static readonly TChar ComponentSeparator = TChar.CreateTruncating(':'); + internal static unsafe (int longestSequenceStart, int longestSequenceLength) FindCompressionRange( ReadOnlySpan numbers) => (-1, -1); internal static unsafe bool ShouldHaveIpv4Embedded(ReadOnlySpan numbers) => false; From b9e3a6216ddd683caca02375dc830d7143b27ace Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Tue, 7 May 2024 07:42:49 +0100 Subject: [PATCH 04/43] System.Private.Uri changes These are just the changes required to make System.Private.Uri compile. * Split IPAddressParser and IPv6AddressHelper into multiple files and shared common components between projects. * Updated some of the Uri-specific additions to IPvXAddressHelper files to use spans. * Minor adjustments to project files to support the above. --- .../src/System/Net/IPAddressParser.Common.cs | 47 +++++++++++++++ .../System/Net/IPv6AddressHelper.Common.cs | 2 +- .../src/System.Net.Primitives.csproj | 7 ++- .../src/System/Net/IPAddressParser.cs | 38 +----------- .../System.Net.Primitives.Pal.Tests.csproj | 2 + ...stem.Net.Primitives.UnitTests.Tests.csproj | 2 + .../src/System.Private.Uri.csproj | 1 + .../src/System/IPv4AddressHelper.cs | 58 +++++++++---------- .../src/System/IPv6AddressHelper.cs | 17 +++--- .../System.Private.Uri/src/System/Uri.cs | 24 ++++---- 10 files changed, 108 insertions(+), 90 deletions(-) create mode 100644 src/libraries/Common/src/System/Net/IPAddressParser.Common.cs diff --git a/src/libraries/Common/src/System/Net/IPAddressParser.Common.cs b/src/libraries/Common/src/System/Net/IPAddressParser.Common.cs new file mode 100644 index 00000000000000..e8385542329355 --- /dev/null +++ b/src/libraries/Common/src/System/Net/IPAddressParser.Common.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Numerics; + +namespace System.Net +{ + internal static partial class IPAddressParser + where TChar : unmanaged, IBinaryInteger + { + public const int Octal = 8; + public const int Decimal = 10; + public const int Hex = 16; + + internal const int MaxIPv4StringLength = 15; // 4 numbers separated by 3 periods, with up to 3 digits per number + internal const int MaxIPv6StringLength = 65; + + // Generic constants which are used for trying to parse a single digit as an integer. + private static readonly TChar NumericRangeStartCharacter = TChar.CreateTruncating('0'); + + public static bool IsValidInteger(int numericBase, TChar ch) + { + Debug.Assert(numericBase is Octal or Decimal or Hex); + + return numericBase switch + { + > 0 and < 10 => ch >= NumericRangeStartCharacter && ch - NumericRangeStartCharacter < TChar.CreateTruncating(numericBase), + Hex => HexConverter.IsHexChar(int.CreateTruncating(ch)), + _ => false + }; + } + + public static bool TryParseInteger(int numericBase, TChar ch, out int parsedNumber) + { + bool validNumber = IsValidInteger(numericBase, ch); + + // HexConverter allows digits 1-F to be mapped to integers. The octal/decimal digit range restrictions are performed + // in IsValidInteger. + parsedNumber = validNumber + ? HexConverter.FromChar(int.CreateTruncating(ch)) + : -1; + + return validNumber; + } + } +} diff --git a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs index 4f50c2f505e5f9..885b7a2bb68a1b 100644 --- a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs @@ -312,7 +312,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) // Nothing // - internal static void Parse(ReadOnlySpan address, Span numbers, out ReadOnlySpan scopeId) + internal static void Parse(ReadOnlySpan address, scoped Span numbers, out ReadOnlySpan scopeId) { int number = 0; int index = 0; diff --git a/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj b/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj index e71aaa8222e300..b7e9e56026f2de 100644 --- a/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj +++ b/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj @@ -1,4 +1,4 @@ - + $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-unix;$(NetCoreAppCurrent)-browser;$(NetCoreAppCurrent)-wasi;$(NetCoreAppCurrent) @@ -33,7 +33,10 @@ - + + diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs index fa70b97a7edfee..03d659b0c82599 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs @@ -11,44 +11,8 @@ namespace System.Net { - internal static class IPAddressParser - where TChar : unmanaged, IBinaryInteger + internal static partial class IPAddressParser { - public const int Octal = 8; - public const int Decimal = 10; - public const int Hex = 16; - - internal const int MaxIPv4StringLength = 15; // 4 numbers separated by 3 periods, with up to 3 digits per number - internal const int MaxIPv6StringLength = 65; - - // Generic constants which are used for trying to parse a single digit as an integer. - private static readonly TChar NumericRangeStartCharacter = TChar.CreateTruncating('0'); - - public static bool IsValidInteger(int numericBase, TChar ch) - { - Debug.Assert(numericBase is Octal or Decimal or Hex); - - return numericBase switch - { - > 0 and < 10 => ch >= NumericRangeStartCharacter && ch - NumericRangeStartCharacter < TChar.CreateTruncating(numericBase), - Hex => HexConverter.IsHexChar(int.CreateTruncating(ch)), - _ => false - }; - } - - public static bool TryParseInteger(int numericBase, TChar ch, out int parsedNumber) - { - bool validNumber = IsValidInteger(numericBase, ch); - - // HexConverter allows digits 1-F to be mapped to integers. The octal/decimal digit range restrictions are performed - // in IsValidInteger. - parsedNumber = validNumber - ? HexConverter.FromChar(int.CreateTruncating(ch)) - : -1; - - return validNumber; - } - internal static IPAddress? Parse(ReadOnlySpan ipSpan, bool tryParse) { Debug.Assert(typeof(TChar) == typeof(byte) || typeof(TChar) == typeof(char)); diff --git a/src/libraries/System.Net.Primitives/tests/PalTests/System.Net.Primitives.Pal.Tests.csproj b/src/libraries/System.Net.Primitives/tests/PalTests/System.Net.Primitives.Pal.Tests.csproj index 90fbc1e3d31bab..459cd8698d105d 100644 --- a/src/libraries/System.Net.Primitives/tests/PalTests/System.Net.Primitives.Pal.Tests.csproj +++ b/src/libraries/System.Net.Primitives/tests/PalTests/System.Net.Primitives.Pal.Tests.csproj @@ -20,6 +20,8 @@ Link="ProductionCode\System\Net\Sockets\SocketError.cs" /> + + + diff --git a/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs b/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs index 1f751eeb57658c..05ed93e012eb4b 100644 --- a/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs +++ b/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs @@ -3,33 +3,30 @@ using System.Diagnostics; -namespace System +namespace System.Net { // The class designed as to keep minimal the working set of Uri class. // The idea is to stay with static helper methods and strings - internal static partial class IPv4AddressHelper + internal static partial class IPv4AddressHelper { // methods // Parse and canonicalize - internal static string ParseCanonicalName(string str, int start, int end, ref bool isLoopback) + internal static string ParseCanonicalName(ReadOnlySpan str, ref bool isLoopback) { - unsafe - { - byte* numbers = stackalloc byte[NumberOfLabels]; - isLoopback = Parse(str, numbers, start, end); + Span numbers = stackalloc byte[NumberOfLabels]; + isLoopback = Parse(str, numbers); - Span stackSpace = stackalloc char[NumberOfLabels * 3 + 3]; - int totalChars = 0, charsWritten; - for (int i = 0; i < 3; i++) - { - numbers[i].TryFormat(stackSpace.Slice(totalChars), out charsWritten); - int periodPos = totalChars + charsWritten; - stackSpace[periodPos] = '.'; - totalChars = periodPos + 1; - } - numbers[3].TryFormat(stackSpace.Slice(totalChars), out charsWritten); - return new string(stackSpace.Slice(0, totalChars + charsWritten)); + Span stackSpace = stackalloc char[NumberOfLabels * 3 + 3]; + int totalChars = 0, charsWritten; + for (int i = 0; i < 3; i++) + { + numbers[i].TryFormat(stackSpace.Slice(totalChars), out charsWritten); + int periodPos = totalChars + charsWritten; + stackSpace[periodPos] = '.'; + totalChars = periodPos + 1; } + numbers[3].TryFormat(stackSpace.Slice(totalChars), out charsWritten); + return new string(stackSpace.Slice(0, totalChars + charsWritten)); } // @@ -37,23 +34,20 @@ internal static string ParseCanonicalName(string str, int start, int end, ref bo // // Convert this IPv4 address into a sequence of 4 8-bit numbers // - private static unsafe bool Parse(string name, byte* numbers, int start, int end) + private static bool Parse(ReadOnlySpan name, Span numbers) { - fixed (char* ipString = name) - { - // end includes ports, so changedEnd may be different from end - int changedEnd = end; - long result = IPv4AddressHelper.ParseNonCanonical(ipString, start, ref changedEnd, true); + // "name" parameter includes ports, so bytesConsumed may be different from span length + int bytesConsumed = 0; + long result = ParseNonCanonical(name, ref bytesConsumed, true); - Debug.Assert(result != Invalid, $"Failed to parse after already validated: {name}"); + Debug.Assert(result != Invalid, $"Failed to parse after already validated: {string.Join(string.Empty, name.ToArray())}"); - unchecked - { - numbers[0] = (byte)(result >> 24); - numbers[1] = (byte)(result >> 16); - numbers[2] = (byte)(result >> 8); - numbers[3] = (byte)(result); - } + unchecked + { + numbers[0] = (byte)(result >> 24); + numbers[1] = (byte)(result >> 16); + numbers[2] = (byte)(result >> 8); + numbers[3] = (byte)(result); } return numbers[0] == 127; diff --git a/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs b/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs index b684f0507bc425..e8c7994453d069 100644 --- a/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs +++ b/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs @@ -3,17 +3,17 @@ using System.Diagnostics; -namespace System +namespace System.Net { // The class designed as to keep minimal the working set of Uri class. // The idea is to stay with static helper methods and strings - internal static partial class IPv6AddressHelper + internal static partial class IPv6AddressHelper { - internal static unsafe string ParseCanonicalName(string str, int start, ref bool isLoopback, ref string? scopeId) + internal static string ParseCanonicalName(ReadOnlySpan str, ref bool isLoopback, out ReadOnlySpan scopeId) { Span numbers = stackalloc ushort[NumberOfLabels]; numbers.Clear(); - Parse(str, numbers, start, ref scopeId); + Parse(str, numbers, out scopeId); isLoopback = IsLoopback(numbers); // RFC 5952 Sections 4 & 5 - Compressed, lower case, with possible embedded IPv4 addresses. @@ -87,7 +87,7 @@ internal static unsafe string ParseCanonicalName(string str, int start, ref bool return new string(stackSpace.Slice(0, pos)); } - private static unsafe bool IsLoopback(ReadOnlySpan numbers) + private static bool IsLoopback(ReadOnlySpan numbers) { // // is the address loopback? Loopback is defined as one of: @@ -238,18 +238,21 @@ private static unsafe bool InternalIsValid(char* name, int start, ref int end, b break; case '.': + int ipv4AddressLength = i; + if (haveIPv4Address) { return false; } - i = end; - if (!IPv4AddressHelper.IsValid(name, lastSequence, ref i, true, false, false)) + if (!IPv4AddressHelper.IsValid(new ReadOnlySpan(name + lastSequence, i - lastSequence), ref ipv4AddressLength, true, false, false)) { return false; } + i = lastSequence + ipv4AddressLength; // ipv4 address takes 2 slots in ipv6 address, one was just counted meeting the '.' ++sequenceCount; + lastSequence = i - sequenceLength; haveIPv4Address = true; --i; // it will be incremented back on the next loop break; diff --git a/src/libraries/System.Private.Uri/src/System/Uri.cs b/src/libraries/System.Private.Uri/src/System/Uri.cs index 354fe4962fb9ac..8661d676ce4604 100644 --- a/src/libraries/System.Private.Uri/src/System/Uri.cs +++ b/src/libraries/System.Private.Uri/src/System/Uri.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Net; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization; @@ -1275,17 +1276,17 @@ public static UriHostNameType CheckHostName(string? name) if (name.StartsWith('[') && name.EndsWith(']')) { // we require that _entire_ name is recognized as ipv6 address - if (IPv6AddressHelper.IsValid(fixedName, 1, ref end) && end == name.Length) + if (IPv6AddressHelper.IsValid(fixedName, 1, ref end) && end == name.Length) { return UriHostNameType.IPv6; } } + } - end = name.Length; - if (IPv4AddressHelper.IsValid(fixedName, 0, ref end, false, false, false) && end == name.Length) - { - return UriHostNameType.IPv4; - } + end = name.Length; + if (IPv4AddressHelper.IsValid(name.AsSpan(), ref end, false, false, false) && end == name.Length) + { + return UriHostNameType.IPv4; } if (DomainNameHelper.IsValid(name, iri: false, notImplicitFile: false, out int length) && length == name.Length) @@ -1304,7 +1305,7 @@ public static UriHostNameType CheckHostName(string? name) name = "[" + name + "]"; fixed (char* newFixedName = name) { - if (IPv6AddressHelper.IsValid(newFixedName, 1, ref end) && end == name.Length) + if (IPv6AddressHelper.IsValid(newFixedName, 1, ref end) && end == name.Length) { return UriHostNameType.IPv6; } @@ -2514,11 +2515,12 @@ private static string CreateHostStringHelper(string str, int idx, int end, ref F case Flags.IPv6HostType: // The helper will return [...] string that is not suited for Dns.Resolve() - host = IPv6AddressHelper.ParseCanonicalName(str, idx, ref loopback, ref scopeId); + host = IPv6AddressHelper.ParseCanonicalName(str.AsSpan(idx), ref loopback, out ReadOnlySpan scopeIdSpan); + scopeId = scopeIdSpan.IsEmpty ? null : new string(scopeIdSpan); break; case Flags.IPv4HostType: - host = IPv4AddressHelper.ParseCanonicalName(str, idx, end, ref loopback); + host = IPv4AddressHelper.ParseCanonicalName(str.AsSpan(idx, end - idx), ref loopback); break; case Flags.UncHostType: @@ -3850,7 +3852,7 @@ private unsafe int CheckAuthorityHelper(char* pString, int idx, int length, } if (ch == '[' && syntax.InFact(UriSyntaxFlags.AllowIPv6Host) && - IPv6AddressHelper.IsValid(pString, start + 1, ref end)) + IPv6AddressHelper.IsValid(pString, start + 1, ref end)) { flags |= Flags.IPv6HostType; @@ -3860,7 +3862,7 @@ private unsafe int CheckAuthorityHelper(char* pString, int idx, int length, } } else if (char.IsAsciiDigit(ch) && syntax.InFact(UriSyntaxFlags.AllowIPv4Host) && - IPv4AddressHelper.IsValid(pString, start, ref end, false, StaticNotAny(flags, Flags.ImplicitFile), syntax.InFact(UriSyntaxFlags.V1_UnknownUri))) + IPv4AddressHelper.IsValid(new ReadOnlySpan(pString + start, end - start), ref end, false, StaticNotAny(flags, Flags.ImplicitFile), syntax.InFact(UriSyntaxFlags.V1_UnknownUri))) { flags |= Flags.IPv4HostType; From 4e580ac6684a8dc8d5189eba35153e3b42078d1d Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Mon, 13 May 2024 02:41:33 +0100 Subject: [PATCH 05/43] Post-Uri testing, corrected additional uses of IPvXAddressHelper --- .../src/System/Net/IPAddressParser.Common.cs | 2 +- .../System/Net/Security/TargetHostNameHelper.cs | 15 ++++----------- .../src/System.Net.Primitives.csproj | 2 +- .../System.Net.Quic/src/System.Net.Quic.csproj | 1 + .../src/System.Net.Security.csproj | 2 ++ .../System.Net.Security.Unit.Tests.csproj | 2 ++ .../src/System/IPv6AddressHelper.cs | 2 +- .../System.Private.Uri/src/System/Uri.cs | 7 +++++-- 8 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/libraries/Common/src/System/Net/IPAddressParser.Common.cs b/src/libraries/Common/src/System/Net/IPAddressParser.Common.cs index e8385542329355..8ca2b7c1f1d617 100644 --- a/src/libraries/Common/src/System/Net/IPAddressParser.Common.cs +++ b/src/libraries/Common/src/System/Net/IPAddressParser.Common.cs @@ -25,7 +25,7 @@ public static bool IsValidInteger(int numericBase, TChar ch) return numericBase switch { - > 0 and < 10 => ch >= NumericRangeStartCharacter && ch - NumericRangeStartCharacter < TChar.CreateTruncating(numericBase), + > 0 and <= 10 => ch >= NumericRangeStartCharacter && ch - NumericRangeStartCharacter < TChar.CreateTruncating(numericBase), Hex => HexConverter.IsHexChar(int.CreateTruncating(ch)), _ => false }; diff --git a/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs b/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs index 7202cebe142bab..572729570e0255 100644 --- a/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs +++ b/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs @@ -49,29 +49,22 @@ internal static unsafe bool IsValidAddress(string? hostname) ReadOnlySpan ipSpan = hostname.AsSpan(); - int end = ipSpan.Length; - if (ipSpan.Contains(':')) { // The address is parsed as IPv6 if and only if it contains a colon. This is valid because // we don't support/parse a port specification at the end of an IPv4 address. Span numbers = stackalloc ushort[IPAddressParserStatics.IPv6AddressShorts]; - fixed (char* ipStringPtr = &MemoryMarshal.GetReference(ipSpan)) - { - return IPv6AddressHelper.IsValidStrict(ipStringPtr, 0, ref end); - } + return IPv6AddressHelper.IsValidStrict(ipSpan); } else if (char.IsDigit(ipSpan[0])) { long tmpAddr; + int end = ipSpan.Length; - fixed (char* ipStringPtr = &MemoryMarshal.GetReference(ipSpan)) - { - tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipStringPtr, 0, ref end, notImplicitFile: true); - } + tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipSpan, ref end, notImplicitFile: true); - if (tmpAddr != IPv4AddressHelper.Invalid && end == ipSpan.Length) + if (tmpAddr != IPv4AddressHelper.Invalid && end == ipSpan.Length) { return true; } diff --git a/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj b/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj index b7e9e56026f2de..2949097341d30f 100644 --- a/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj +++ b/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj @@ -34,7 +34,7 @@ + Link="System\Net\IPAddressParser.Common.cs" /> diff --git a/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj b/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj index 8245e3304ad69c..aa897ce599d461 100644 --- a/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj +++ b/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj @@ -40,6 +40,7 @@ + diff --git a/src/libraries/System.Net.Security/src/System.Net.Security.csproj b/src/libraries/System.Net.Security/src/System.Net.Security.csproj index e6650f80670e17..5aeff53ac83aa0 100644 --- a/src/libraries/System.Net.Security/src/System.Net.Security.csproj +++ b/src/libraries/System.Net.Security/src/System.Net.Security.csproj @@ -71,6 +71,8 @@ + + .IsValid(new ReadOnlySpan(name + lastSequence, i - lastSequence), ref ipv4AddressLength, true, false, false)) + if (!IPv4AddressHelper.IsValid(new ReadOnlySpan(name + lastSequence, end - lastSequence), ref ipv4AddressLength, true, false, false)) { return false; } diff --git a/src/libraries/System.Private.Uri/src/System/Uri.cs b/src/libraries/System.Private.Uri/src/System/Uri.cs index 8661d676ce4604..22f7c8e0e81911 100644 --- a/src/libraries/System.Private.Uri/src/System/Uri.cs +++ b/src/libraries/System.Private.Uri/src/System/Uri.cs @@ -2520,7 +2520,7 @@ private static string CreateHostStringHelper(string str, int idx, int end, ref F break; case Flags.IPv4HostType: - host = IPv4AddressHelper.ParseCanonicalName(str.AsSpan(idx, end - idx), ref loopback); + host = IPv4AddressHelper.ParseCanonicalName(str.AsSpan(idx), ref loopback); break; case Flags.UncHostType: @@ -3851,6 +3851,8 @@ private unsafe int CheckAuthorityHelper(char* pString, int idx, int length, } } + int bytesConsumed = 0; + if (ch == '[' && syntax.InFact(UriSyntaxFlags.AllowIPv6Host) && IPv6AddressHelper.IsValid(pString, start + 1, ref end)) { @@ -3862,8 +3864,9 @@ private unsafe int CheckAuthorityHelper(char* pString, int idx, int length, } } else if (char.IsAsciiDigit(ch) && syntax.InFact(UriSyntaxFlags.AllowIPv4Host) && - IPv4AddressHelper.IsValid(new ReadOnlySpan(pString + start, end - start), ref end, false, StaticNotAny(flags, Flags.ImplicitFile), syntax.InFact(UriSyntaxFlags.V1_UnknownUri))) + IPv4AddressHelper.IsValid(new ReadOnlySpan(pString + start, end - start), ref bytesConsumed, false, StaticNotAny(flags, Flags.ImplicitFile), syntax.InFact(UriSyntaxFlags.V1_UnknownUri))) { + end = start + bytesConsumed; flags |= Flags.IPv4HostType; if (hasUnicode) From 8ff72415a1fb599f9f60cbd6e38e7cce22d6d2c3 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Mon, 13 May 2024 03:52:13 +0100 Subject: [PATCH 06/43] Added IUtf8SpanParsable unit tests --- .../tests/FunctionalTests/IPAddressParsing.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsing.cs b/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsing.cs index 956c895d1ab8cc..8a992abd015b76 100644 --- a/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsing.cs +++ b/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsing.cs @@ -142,6 +142,26 @@ public sealed class IPAddressParsingFormatting_ISpanParsable_ISpanFormattable : private static bool TryParse(string s, out T result) where T : ISpanParsable => T.TryParse(s.AsSpan(), null, out result); } + public sealed class IPAddressParsingFormatting_IUtf8SpanParsable_IUtf8SpanFormattable : IPAddressParsingFormatting_Span + { + public override IPAddress Parse(string ipString) => Parse(ipString); + public override bool TryParse(string ipString, out IPAddress address) => TryParse(ipString, out address); + public override bool TryFormat(IPAddress address, Span utf8Destination, out int bytesWritten) => ((IUtf8SpanFormattable)address).TryFormat(utf8Destination, out bytesWritten, default, null); + + private static T Parse(string s) where T : IUtf8SpanParsable + { + byte[] utf8Bytes = Encoding.UTF8.GetBytes(s); + + return T.Parse(utf8Bytes.AsSpan(), null); + } + private static bool TryParse(string s, out T result) where T : IUtf8SpanParsable + { + byte[] utf8Bytes = Encoding.UTF8.GetBytes(s); + + return T.TryParse(utf8Bytes.AsSpan(), null, out result); + } + } + public abstract class IPAddressParsingFormatting { public abstract IPAddress Parse(string ipString); From f627e448b55167039132461a27cf93243e058d45 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Mon, 13 May 2024 04:38:14 +0100 Subject: [PATCH 07/43] Implemented IUtf8SpanParsable on IPNetwork, added tests --- .../ref/System.Net.Primitives.cs | 6 +- .../src/System/Net/IPNetwork.cs | 55 ++++++++++++++++++- .../tests/FunctionalTests/IPNetworkTest.cs | 25 ++++++++- 3 files changed, 81 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs b/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs index bb1b6c06254deb..75b317a188876e 100644 --- a/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs +++ b/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs @@ -298,7 +298,7 @@ public IPEndPoint(System.Net.IPAddress address, int port) { } public static bool TryParse(System.ReadOnlySpan s, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Net.IPEndPoint? result) { throw null; } public static bool TryParse(string s, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Net.IPEndPoint? result) { throw null; } } - public readonly partial struct IPNetwork : System.IEquatable, System.IFormattable, System.IParsable, System.ISpanFormattable, System.ISpanParsable, System.IUtf8SpanFormattable + public readonly partial struct IPNetwork : System.IEquatable, System.IFormattable, System.IParsable, System.ISpanFormattable, System.ISpanParsable, System.IUtf8SpanFormattable, System.IUtf8SpanParsable { private readonly object _dummy; private readonly int _dummyPrimitive; @@ -313,6 +313,7 @@ public IPEndPoint(System.Net.IPAddress address, int port) { } public static bool operator !=(System.Net.IPNetwork left, System.Net.IPNetwork right) { throw null; } public static System.Net.IPNetwork Parse(System.ReadOnlySpan s) { throw null; } public static System.Net.IPNetwork Parse(string s) { throw null; } + public static IPNetwork Parse(ReadOnlySpan utf8Text) { throw null; } string System.IFormattable.ToString(string? format, System.IFormatProvider? provider) { throw null; } static System.Net.IPNetwork System.IParsable.Parse([System.Diagnostics.CodeAnalysis.NotNullAttribute] string s, System.IFormatProvider? provider) { throw null; } static bool System.IParsable.TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, System.IFormatProvider? provider, out System.Net.IPNetwork result) { throw null; } @@ -320,11 +321,14 @@ public IPEndPoint(System.Net.IPAddress address, int port) { } bool System.IUtf8SpanFormattable.TryFormat(System.Span utf8Destination, out int bytesWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) { throw null; } static System.Net.IPNetwork System.ISpanParsable.Parse(System.ReadOnlySpan s, System.IFormatProvider? provider) { throw null; } static bool System.ISpanParsable.TryParse(System.ReadOnlySpan s, System.IFormatProvider? provider, out System.Net.IPNetwork result) { throw null; } + static System.Net.IPNetwork System.IUtf8SpanParsable.Parse(System.ReadOnlySpan utf8Text, System.IFormatProvider? provider) { throw null; } + static bool System.IUtf8SpanParsable.TryParse(System.ReadOnlySpan utf8Text, System.IFormatProvider? provider, out System.Net.IPNetwork result) { throw null; } public override string ToString() { throw null; } public bool TryFormat(System.Span destination, out int charsWritten) { throw null; } public bool TryFormat(System.Span utf8Destination, out int bytesWritten) { throw null; } public static bool TryParse(System.ReadOnlySpan s, out System.Net.IPNetwork result) { throw null; } public static bool TryParse(string? s, out System.Net.IPNetwork result) { throw null; } + public static bool TryParse(ReadOnlySpan utf8Text, [MaybeNullWhen(false)] out IPNetwork result) { throw null; } } public partial interface IWebProxy { diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPNetwork.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPNetwork.cs index 1c3a3acd42932e..65718ad2904d27 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPNetwork.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPNetwork.cs @@ -21,7 +21,7 @@ namespace System.Net /// In other words, is always the first usable address of the network. /// The constructor and the parsing methods will throw in case there are non-zero bits after the prefix. /// - public readonly struct IPNetwork : IEquatable, ISpanFormattable, ISpanParsable, IUtf8SpanFormattable + public readonly struct IPNetwork : IEquatable, ISpanFormattable, ISpanParsable, IUtf8SpanFormattable, IUtf8SpanParsable { private readonly IPAddress? _baseAddress; @@ -154,6 +154,22 @@ public static IPNetwork Parse(ReadOnlySpan s) return result; } + /// + /// Converts a UTF-8 CIDR character span to an instance. + /// + /// A UTF-8 character span that defines an IP network in CIDR notation. + /// An instance. + /// is not a valid UTF-8 CIDR network string, or the address contains non-zero bits after the network prefix. + public static IPNetwork Parse(ReadOnlySpan utf8Text) + { + if (!TryParse(utf8Text, out IPNetwork result)) + { + throw new FormatException(SR.net_bad_ip_network); + } + + return result; + } + /// /// Converts the specified CIDR string to an instance and returns a value indicating whether the conversion succeeded. /// @@ -176,7 +192,7 @@ public static bool TryParse(string? s, out IPNetwork result) /// /// A that defines an IP network in CIDR notation. /// When the method returns, contains an instance if the conversion succeeds. - /// if the conversion was succesful; otherwise, . + /// if the conversion was successful; otherwise, . public static bool TryParse(ReadOnlySpan s, out IPNetwork result) { int separatorIndex = s.LastIndexOf('/'); @@ -200,6 +216,35 @@ public static bool TryParse(ReadOnlySpan s, out IPNetwork result) return false; } + /// + /// Converts the specified UTF-8 CIDR character span to an instance and returns a value indicating whether the conversion succeeded. + /// + /// A UTF-8 character span that defines an IP network in CIDR notation. + /// When the method returns, contains an instance if the conversion succeeds. + /// if the conversion was successful; otherwise, . + public static bool TryParse(ReadOnlySpan utf8Text, [MaybeNullWhen(false)] out IPNetwork result) + { + int separatorIndex = utf8Text.IndexOf(byte.CreateTruncating('/')); + if (separatorIndex >= 0) + { + ReadOnlySpan ipAddressSpan = utf8Text.Slice(0, separatorIndex); + ReadOnlySpan prefixLengthSpan = utf8Text.Slice(separatorIndex + 1); + + if (IPAddress.TryParse(ipAddressSpan, CultureInfo.InvariantCulture, out IPAddress? address) && + int.TryParse(prefixLengthSpan, NumberStyles.None, CultureInfo.InvariantCulture, out int prefixLength) && + prefixLength <= GetMaxPrefixLength(address) && + !HasNonZeroBitsAfterNetworkPrefix(address, prefixLength)) + { + Debug.Assert(prefixLength >= 0); // Parsing with NumberStyles.None should ensure that prefixLength is always non-negative. + result = new IPNetwork(address, prefixLength, false); + return true; + } + } + + result = default; + return false; + } + private static int GetMaxPrefixLength(IPAddress baseAddress) => baseAddress.AddressFamily == AddressFamily.InterNetwork ? 32 : 128; private static bool HasNonZeroBitsAfterNetworkPrefix(IPAddress baseAddress, int prefixLength) @@ -324,7 +369,13 @@ bool IUtf8SpanFormattable.TryFormat(Span utf8Destination, out int bytesWri /// static IPNetwork ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s); + /// + static IPNetwork IUtf8SpanParsable.Parse(ReadOnlySpan utf8Text, IFormatProvider? provider) => Parse(utf8Text); + /// static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, out IPNetwork result) => TryParse(s, out result); + + /// + static bool IUtf8SpanParsable.TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out IPNetwork result) => TryParse(utf8Text, out result); } } diff --git a/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPNetworkTest.cs b/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPNetworkTest.cs index 6688b7a7864343..6e61879bdedb2e 100644 --- a/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPNetworkTest.cs +++ b/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPNetworkTest.cs @@ -92,44 +92,65 @@ public void Constructor_NonZeroBitsAfterNetworkPrefix_ThrowsArgumentException(st [MemberData(nameof(IncorrectFormatData))] public void Parse_IncorrectFormat_ThrowsFormatException(string input) { + byte[] utf8Bytes = Encoding.UTF8.GetBytes(input); + Assert.Throws(() => IPNetwork.Parse(input)); + Assert.Throws(() => IPNetwork.Parse(utf8Bytes)); } [Theory] [MemberData(nameof(IncorrectFormatData))] public void TryParse_IncorrectFormat_ReturnsFalse(string input) { + byte[] utf8Bytes = Encoding.UTF8.GetBytes(input); + Assert.False(IPNetwork.TryParse(input, out _)); + Assert.False(IPNetwork.TryParse(utf8Bytes, out _)); } [Theory] [MemberData(nameof(InvalidNetworkNotationData))] public void Parse_InvalidNetworkNotation_ThrowsFormatException(string input) { + byte[] utf8Bytes = Encoding.UTF8.GetBytes(input); + Assert.Throws(() => IPNetwork.Parse(input)); + Assert.Throws(() => IPNetwork.Parse(utf8Bytes)); } [Theory] [MemberData(nameof(InvalidNetworkNotationData))] public void TryParse_InvalidNetworkNotation_ReturnsFalse(string input) { + byte[] utf8Bytes = Encoding.UTF8.GetBytes(input); + Assert.False(IPNetwork.TryParse(input, out _)); + Assert.False(IPNetwork.TryParse(utf8Bytes, out _)); } [Theory] [MemberData(nameof(ValidIPNetworkData))] public void Parse_ValidNetworkNotation_Succeeds(string input) { - var network = IPNetwork.Parse(input); - Assert.Equal(input, network.ToString()); + byte[] utf8Bytes = Encoding.UTF8.GetBytes(input); + var stringParsedNetwork = IPNetwork.Parse(input); + var utf8ParsedNetwork = IPNetwork.Parse(utf8Bytes); + + Assert.Equal(input, stringParsedNetwork.ToString()); + Assert.Equal(input, utf8ParsedNetwork.ToString()); } [Theory] [MemberData(nameof(ValidIPNetworkData))] public void TryParse_ValidNetworkNotation_Succeeds(string input) { + byte[] utf8Bytes = Encoding.UTF8.GetBytes(input); + Assert.True(IPNetwork.TryParse(input, out IPNetwork network)); Assert.Equal(input, network.ToString()); + + Assert.True(IPNetwork.TryParse(utf8Bytes, out network)); + Assert.Equal(input, network.ToString()); } [Fact] From 1f52945fd40895f7acd4811e9bcd7b68c2df4930 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Mon, 13 May 2024 05:50:12 +0100 Subject: [PATCH 08/43] Brief tidy-up of System.Net.Primitives ref project and csproj Removed a using statement in the ref project, small reduction in the csproj diff for System.Net.Primitives --- .../System.Net.Primitives/ref/System.Net.Primitives.cs | 4 +--- .../System.Net.Primitives/src/System.Net.Primitives.csproj | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs b/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs index 75b317a188876e..b4493310b2cf2d 100644 --- a/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs +++ b/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs @@ -4,8 +4,6 @@ // Changes to this file must follow the https://aka.ms/api-review process. // ------------------------------------------------------------------------------ -using System.Diagnostics.CodeAnalysis; - namespace System.Net { [System.FlagsAttribute] @@ -328,7 +326,7 @@ public IPEndPoint(System.Net.IPAddress address, int port) { } public bool TryFormat(System.Span utf8Destination, out int bytesWritten) { throw null; } public static bool TryParse(System.ReadOnlySpan s, out System.Net.IPNetwork result) { throw null; } public static bool TryParse(string? s, out System.Net.IPNetwork result) { throw null; } - public static bool TryParse(ReadOnlySpan utf8Text, [MaybeNullWhen(false)] out IPNetwork result) { throw null; } + public static bool TryParse(ReadOnlySpan utf8Text, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out IPNetwork result) { throw null; } } public partial interface IWebProxy { diff --git a/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj b/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj index 2949097341d30f..1e90d2c95ffa78 100644 --- a/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj +++ b/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj @@ -35,8 +35,7 @@ - + From 3710a7a436ba1b6d2c98443ff85929951ea82939 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Mon, 13 May 2024 06:07:08 +0100 Subject: [PATCH 09/43] Further cleanup of System.Net.Primitives.Pal.Tests csproj --- .../tests/PalTests/System.Net.Primitives.Pal.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Net.Primitives/tests/PalTests/System.Net.Primitives.Pal.Tests.csproj b/src/libraries/System.Net.Primitives/tests/PalTests/System.Net.Primitives.Pal.Tests.csproj index 459cd8698d105d..4d2f7a7b210dc9 100644 --- a/src/libraries/System.Net.Primitives/tests/PalTests/System.Net.Primitives.Pal.Tests.csproj +++ b/src/libraries/System.Net.Primitives/tests/PalTests/System.Net.Primitives.Pal.Tests.csproj @@ -21,7 +21,7 @@ + Link="ProductionCode\System\Net\IPAddressParser.Common.cs" /> Date: Mon, 13 May 2024 06:11:06 +0100 Subject: [PATCH 10/43] Further cleanup of System.Net.Primitives.UnitTests.Tests csproj --- .../UnitTests/System.Net.Primitives.UnitTests.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Net.Primitives/tests/UnitTests/System.Net.Primitives.UnitTests.Tests.csproj b/src/libraries/System.Net.Primitives/tests/UnitTests/System.Net.Primitives.UnitTests.Tests.csproj index c43ba8028a1c5b..0b4603eb5f2558 100644 --- a/src/libraries/System.Net.Primitives/tests/UnitTests/System.Net.Primitives.UnitTests.Tests.csproj +++ b/src/libraries/System.Net.Primitives/tests/UnitTests/System.Net.Primitives.UnitTests.Tests.csproj @@ -44,7 +44,7 @@ + Link="ProductionCode\System\Net\IPAddressParser.Common.cs" /> Date: Mon, 13 May 2024 20:18:00 +0100 Subject: [PATCH 11/43] Correctly setting bytesConsumed in IPv4AddressHelper.ParseNonCanonical --- .../Common/src/System/Net/IPv4AddressHelper.Common.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs index 1a221d28ce249f..6c7757538c5452 100644 --- a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs @@ -290,24 +290,28 @@ internal static long ParseNonCanonical(ReadOnlySpan name, ref int bytesCo { return Invalid; } + bytesConsumed = current; return parts[0]; case 1: // 0xFF.0xFFFFFF if (parts[1] > 0xffffff) { return Invalid; } + bytesConsumed = current; return (parts[0] << 24) | (parts[1] & 0xffffff); case 2: // 0xFF.0xFF.0xFFFF if (parts[2] > 0xffff) { return Invalid; } + bytesConsumed = current; return (parts[0] << 24) | ((parts[1] & 0xff) << 16) | (parts[2] & 0xffff); case 3: // 0xFF.0xFF.0xFF.0xFF if (parts[3] > 0xff) { return Invalid; } + bytesConsumed = current; return (parts[0] << 24) | ((parts[1] & 0xff) << 16) | ((parts[2] & 0xff) << 8) | (parts[3] & 0xff); default: return Invalid; From ea95067692864a3c20b6db2f08153473d7a4b95b Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Fri, 14 Jun 2024 23:41:25 +0100 Subject: [PATCH 12/43] Changes following API review --- .../ref/System.Net.Primitives.cs | 20 ++++++----- .../src/System/Net/IPAddress.cs | 33 +++++++++++++++---- .../src/System/Net/IPNetwork.cs | 2 +- 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs b/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs index b4493310b2cf2d..545cf638ee7b7e 100644 --- a/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs +++ b/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs @@ -223,7 +223,7 @@ public partial interface ICredentialsByHost { System.Net.NetworkCredential? GetCredential(string host, int port, string authenticationType); } - public partial class IPAddress : ISpanFormattable, ISpanParsable, IUtf8SpanFormattable, IUtf8SpanParsable + public partial class IPAddress : ISpanFormattable, ISpanParsable, IUtf8SpanFormattable, IUtf8SpanParsable { public static readonly System.Net.IPAddress Any; public static readonly System.Net.IPAddress Broadcast; @@ -261,20 +261,22 @@ public IPAddress(System.ReadOnlySpan address, long scopeid) { } public static long NetworkToHostOrder(long network) { throw null; } public static System.Net.IPAddress Parse(System.ReadOnlySpan ipSpan) { throw null; } public static System.Net.IPAddress Parse(string ipString) { throw null; } - public static System.Net.IPAddress Parse(ReadOnlySpan utf8Text, IFormatProvider? provider) { throw null; } - static IPAddress ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) { throw null; } - static IPAddress IParsable.Parse(string s, IFormatProvider? provider) { throw null; } + public static System.Net.IPAddress Parse(System.ReadOnlySpan utf8Text) { throw null; } + static System.Net.IPAddress ISpanParsable.Parse(System.ReadOnlySpan s, System.IFormatProvider? provider) { throw null; } + static System.Net.IPAddress IParsable.Parse(string s, System.IFormatProvider? provider) { throw null; } + static System.Net.IPAddress IUtf8SpanParsable.Parse(System.ReadOnlySpan utf8Text, System.IFormatProvider? provider) { throw null; } public override string ToString() { throw null; } - string IFormattable.ToString(string? format, IFormatProvider? formatProvider) { throw null; } + string System.IFormattable.ToString(string? format, System.IFormatProvider? formatProvider) { throw null; } public bool TryFormat(System.Span destination, out int charsWritten) { throw null; } public bool TryFormat(System.Span utf8Destination, out int bytesWritten) { throw null; } - bool ISpanFormattable.TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) { throw null; } + bool System.ISpanFormattable.TryFormat(System.Span destination, out int charsWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) { throw null; } bool System.IUtf8SpanFormattable.TryFormat(System.Span utf8Destination, out int bytesWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) { throw null; } public static bool TryParse(System.ReadOnlySpan ipSpan, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Net.IPAddress? address) { throw null; } public static bool TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? ipString, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Net.IPAddress? address) { throw null; } - public static bool TryParse(System.ReadOnlySpan utf8Text, System.IFormatProvider? provider, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out System.Net.IPAddress? result) { throw null; } - static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, out IPAddress result) { throw null; } - static bool IParsable.TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, IFormatProvider? provider, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out IPAddress? result) { throw null; } + public static bool TryParse(System.ReadOnlySpan utf8Text, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out System.Net.IPAddress? result) { throw null; } + static bool System.ISpanParsable.TryParse(System.ReadOnlySpan s, System.IFormatProvider? provider, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out System.Net.IPAddress? result) { throw null; } + static bool System.IParsable.TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, System.IFormatProvider? provider, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Net.IPAddress? result) { throw null; } + static bool System.IUtf8SpanParsable.TryParse(System.ReadOnlySpan utf8Text, System.IFormatProvider? provider, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out System.Net.IPAddress? result) { throw null; } public bool TryWriteBytes(System.Span destination, out int bytesWritten) { throw null; } } public partial class IPEndPoint : System.Net.EndPoint diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs index 3275a80121393e..afac03a667100c 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs @@ -232,6 +232,18 @@ public static bool TryParse([NotNullWhen(true)] string? ipString, [NotNullWhen(t return (address != null); } + /// + /// Tries to parse a span of UTF-8 characters into a value. + /// + /// The span of UTF-8 characters to parse. + /// On return, contains the result of successfully parsing or an undefined value on failure. + /// true if was successfully parsed; otherwise, false. + public static bool TryParse(ReadOnlySpan utf8Text, [NotNullWhen(true)] out IPAddress? result) + { + result = IPAddressParser.Parse(utf8Text, tryParse: true); + return (result != null); + } + public static bool TryParse(ReadOnlySpan ipSpan, [NotNullWhen(true)] out IPAddress? address) { address = IPAddressParser.Parse(ipSpan, tryParse: true); @@ -239,11 +251,8 @@ public static bool TryParse(ReadOnlySpan ipSpan, [NotNullWhen(true)] out I } /// - public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, [NotNullWhen(true)] out IPAddress? result) - { - result = IPAddressParser.Parse(utf8Text, tryParse: true); - return (result != null); - } + static bool IUtf8SpanParsable.TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, [NotNullWhen(true)] out IPAddress? result) + => TryParse(utf8Text, out result); /// static bool IParsable.TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [NotNullWhen(true)] out IPAddress? result) => @@ -262,15 +271,25 @@ public static IPAddress Parse(string ipString) return IPAddressParser.Parse(ipString.AsSpan(), tryParse: false)!; } + /// + /// Parses a span of UTF-8 characters into a value. + /// + /// The span of UTF-8 characters to parse. + /// The result of parsing . + public static IPAddress Parse(ReadOnlySpan utf8Text) + { + return IPAddressParser.Parse(utf8Text, tryParse: false)!; + } + public static IPAddress Parse(ReadOnlySpan ipSpan) { return IPAddressParser.Parse(ipSpan, tryParse: false)!; } /// - public static IPAddress Parse(ReadOnlySpan utf8Text, IFormatProvider? provider) => + static IPAddress IUtf8SpanParsable.Parse(ReadOnlySpan utf8Text, IFormatProvider? provider) => // provider is explicitly ignored - IPAddressParser.Parse(utf8Text, tryParse: false)!; + Parse(utf8Text); /// static IPAddress ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPNetwork.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPNetwork.cs index 65718ad2904d27..5786bec55a4d69 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPNetwork.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPNetwork.cs @@ -230,7 +230,7 @@ public static bool TryParse(ReadOnlySpan utf8Text, [MaybeNullWhen(false)] ReadOnlySpan ipAddressSpan = utf8Text.Slice(0, separatorIndex); ReadOnlySpan prefixLengthSpan = utf8Text.Slice(separatorIndex + 1); - if (IPAddress.TryParse(ipAddressSpan, CultureInfo.InvariantCulture, out IPAddress? address) && + if (IPAddress.TryParse(ipAddressSpan, out IPAddress? address) && int.TryParse(prefixLengthSpan, NumberStyles.None, CultureInfo.InvariantCulture, out int prefixLength) && prefixLength <= GetMaxPrefixLength(address) && !HasNonZeroBitsAfterNetworkPrefix(address, prefixLength)) From d478e82782287110e5eae837e2e60b7d87813128 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Wed, 19 Jun 2024 07:36:47 +0100 Subject: [PATCH 13/43] Code review changes (round 1) --- .../src/System/Net/IPAddressParser.Common.cs | 20 +++++++++---------- .../System/Net/IPv4AddressHelper.Common.cs | 4 ++-- .../System/Net/IPv6AddressHelper.Common.cs | 6 +----- .../src/System/Net/IPAddressParser.cs | 9 +-------- 4 files changed, 13 insertions(+), 26 deletions(-) diff --git a/src/libraries/Common/src/System/Net/IPAddressParser.Common.cs b/src/libraries/Common/src/System/Net/IPAddressParser.Common.cs index 8ca2b7c1f1d617..86cf9dde9f5d7f 100644 --- a/src/libraries/Common/src/System/Net/IPAddressParser.Common.cs +++ b/src/libraries/Common/src/System/Net/IPAddressParser.Common.cs @@ -16,29 +16,27 @@ internal static partial class IPAddressParser internal const int MaxIPv4StringLength = 15; // 4 numbers separated by 3 periods, with up to 3 digits per number internal const int MaxIPv6StringLength = 65; - // Generic constants which are used for trying to parse a single digit as an integer. - private static readonly TChar NumericRangeStartCharacter = TChar.CreateTruncating('0'); - public static bool IsValidInteger(int numericBase, TChar ch) + => IsValidInteger(numericBase, int.CreateTruncating(ch)); + + private static bool IsValidInteger(int numericBase, int characterValue) { Debug.Assert(numericBase is Octal or Decimal or Hex); - return numericBase switch - { - > 0 and <= 10 => ch >= NumericRangeStartCharacter && ch - NumericRangeStartCharacter < TChar.CreateTruncating(numericBase), - Hex => HexConverter.IsHexChar(int.CreateTruncating(ch)), - _ => false - }; + return numericBase <= Decimal + ? characterValue >= '0' && characterValue < '0' + numericBase + : HexConverter.IsHexChar(characterValue); } public static bool TryParseInteger(int numericBase, TChar ch, out int parsedNumber) { - bool validNumber = IsValidInteger(numericBase, ch); + int characterValue = int.CreateTruncating(ch); + bool validNumber = IsValidInteger(numericBase, characterValue); // HexConverter allows digits 1-F to be mapped to integers. The octal/decimal digit range restrictions are performed // in IsValidInteger. parsedNumber = validNumber - ? HexConverter.FromChar(int.CreateTruncating(ch)) + ? HexConverter.FromChar(characterValue) : -1; return validNumber; diff --git a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs index 6c7757538c5452..e554b1025ace25 100644 --- a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs @@ -269,8 +269,8 @@ internal static long ParseNonCanonical(ReadOnlySpan name, ref int bytesCo { // end of string, allowed } - else if ((Array.IndexOf(PrefixSeparators, name[current]) != -1) - || (notImplicitFile && (Array.IndexOf(UrlSeparators, name[current]) != -1))) + else if ((!notImplicitFile && (name[current] == PrefixSeparators[0] || name[current] == PrefixSeparators[1])) + || (notImplicitFile && (name[current] == UrlSeparators[0] || name[current] == UrlSeparators[1] || name[current] == UrlSeparators[2]))) { bytesConsumed = current; } diff --git a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs index 885b7a2bb68a1b..6a44a503ed0ddb 100644 --- a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs @@ -398,13 +398,9 @@ internal static void Parse(ReadOnlySpan address, scoped Span numb } } } - else if (IPAddressParser.TryParseInteger(IPAddressParser.Hex, address[i++], out int digit)) - { - number = number * IPAddressParser.Hex + digit; - } else { - throw new ArgumentException(null, nameof(digit)); + number = number * IPAddressParser.Hex + Uri.FromHex((char)short.CreateChecked(address[i++])); } } diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs index 03d659b0c82599..b3a61bdaaad1db 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs @@ -5,7 +5,6 @@ using System.Globalization; using System.Net.NetworkInformation; using System.Net.Sockets; -using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -63,6 +62,7 @@ private static bool TryParseIpv4(ReadOnlySpan ipSpan, out long address) private static bool TryParseIPv6(ReadOnlySpan ipSpan, Span numbers, int numbersLength, out uint scope) { + Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); Debug.Assert(numbersLength >= IPAddressParserStatics.IPv6AddressShorts); bool isValid = IPv6AddressHelper.IsValidStrict(ipSpan); @@ -102,13 +102,6 @@ private static bool TryParseIPv6(ReadOnlySpan ipSpan, Span number interfaceName = new string(castScopeIdSpan); } - // This can only happen if TChar is neither char nor byte. This is protected against by this method's - // only call site. - if (string.IsNullOrEmpty(interfaceName)) - { - return false; - } - uint interfaceIndex = InterfaceInfoPal.InterfaceNameToIndex(interfaceName); if (interfaceIndex > 0) { From c42c706021f0a5e5f5c8b7c5f27d74b06f4b44e4 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 22 Jun 2024 09:50:55 +0100 Subject: [PATCH 14/43] Removed generic type definition from classes Moved the generic type definition from the IP*AddressParser classes to individual methods, and moved the constant-value fields to local variables. Swapped out the arrays in these constant-value fields to a ReadOnlySpan to reduce allocations. Replaced calls to TChar.CreateTruncating with TChar.CreateChecked. --- .../src/System/Net/IPAddressParser.Common.cs | 9 +-- .../System/Net/IPv4AddressHelper.Common.cs | 60 ++++++++++-------- .../System/Net/IPv6AddressHelper.Common.cs | 61 +++++++++++-------- .../Net/Security/TargetHostNameHelper.cs | 6 +- .../src/System/Net/IPAddress.cs | 34 +++++------ .../src/System/Net/IPAddressParser.cs | 34 +++++++---- .../UnitTests/Fakes/IPv4AddressHelper.cs | 7 ++- .../UnitTests/Fakes/IPv6AddressHelper.cs | 12 ++-- .../src/System/IPv4AddressHelper.cs | 9 ++- .../src/System/IPv6AddressHelper.cs | 8 ++- .../System.Private.Uri/src/System/Uri.cs | 14 ++--- 11 files changed, 144 insertions(+), 110 deletions(-) diff --git a/src/libraries/Common/src/System/Net/IPAddressParser.Common.cs b/src/libraries/Common/src/System/Net/IPAddressParser.Common.cs index 86cf9dde9f5d7f..11122b325b8a26 100644 --- a/src/libraries/Common/src/System/Net/IPAddressParser.Common.cs +++ b/src/libraries/Common/src/System/Net/IPAddressParser.Common.cs @@ -6,8 +6,7 @@ namespace System.Net { - internal static partial class IPAddressParser - where TChar : unmanaged, IBinaryInteger + internal static partial class IPAddressParser { public const int Octal = 8; public const int Decimal = 10; @@ -16,7 +15,8 @@ internal static partial class IPAddressParser internal const int MaxIPv4StringLength = 15; // 4 numbers separated by 3 periods, with up to 3 digits per number internal const int MaxIPv6StringLength = 65; - public static bool IsValidInteger(int numericBase, TChar ch) + public static bool IsValidInteger(int numericBase, TChar ch) + where TChar : unmanaged, IBinaryInteger => IsValidInteger(numericBase, int.CreateTruncating(ch)); private static bool IsValidInteger(int numericBase, int characterValue) @@ -28,7 +28,8 @@ private static bool IsValidInteger(int numericBase, int characterValue) : HexConverter.IsHexChar(characterValue); } - public static bool TryParseInteger(int numericBase, TChar ch, out int parsedNumber) + public static bool TryParseInteger(int numericBase, TChar ch, out int parsedNumber) + where TChar : unmanaged, IBinaryInteger { int characterValue = int.CreateTruncating(ch); bool validNumber = IsValidInteger(numericBase, characterValue); diff --git a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs index e554b1025ace25..1ef8f45c22ebcd 100644 --- a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs @@ -7,23 +7,16 @@ namespace System.Net { - internal static partial class IPv4AddressHelper - where TChar : unmanaged, IBinaryInteger + internal static partial class IPv4AddressHelper { - // IPv4 address-specific generic constants. - public static readonly TChar ComponentSeparator = TChar.CreateTruncating('.'); - public static readonly TChar[] PrefixSeparators = [TChar.CreateTruncating('/'), TChar.CreateTruncating('\\')]; - public static readonly TChar[] UrlSeparators = [TChar.CreateTruncating(':'), TChar.CreateTruncating('?'), TChar.CreateTruncating('#')]; - public static readonly TChar[] HexadecimalPrefix = [TChar.CreateTruncating('0'), TChar.CreateTruncating('x')]; - public static readonly TChar OctalPrefix = HexadecimalPrefix[0]; - internal const long Invalid = -1; private const long MaxIPv4Value = uint.MaxValue; // the native parser cannot handle MaxIPv4Value, only MaxIPv4Value - 1 private const int NumberOfLabels = 4; // Only called from the IPv6Helper, only parse the canonical format - internal static int ParseHostNumber(ReadOnlySpan str) + internal static int ParseHostNumber(ReadOnlySpan str) + where TChar : unmanaged, IBinaryInteger { Span numbers = stackalloc byte[NumberOfLabels]; int start = 0; @@ -33,9 +26,9 @@ internal static int ParseHostNumber(ReadOnlySpan str) int b = 0; TChar ch; - for (; (start < str.Length) && (ch = str[start]) != TChar.CreateTruncating('.') && ch != TChar.CreateTruncating(':'); ++start) + for (; (start < str.Length) && (ch = str[start]) != TChar.CreateChecked('.') && ch != TChar.CreateChecked(':'); ++start) { - b = (b * 10) + int.CreateTruncating(ch - TChar.CreateTruncating('0')); + b = (b * 10) + int.CreateTruncating(ch - TChar.CreateChecked('0')); } numbers[i] = (byte)b; @@ -87,7 +80,8 @@ internal static int ParseHostNumber(ReadOnlySpan str) // //Remark: MUST NOT be used unless all input indexes are verified and trusted. - internal static bool IsValid(ReadOnlySpan name, ref int bytesConsumed, bool allowIPv6, bool notImplicitFile, bool unknownScheme) + internal static bool IsValid(ReadOnlySpan name, ref int bytesConsumed, bool allowIPv6, bool notImplicitFile, bool unknownScheme) + where TChar : unmanaged, IBinaryInteger { // IPv6 can only have canonical IPv4 embedded. Unknown schemes will not attempt parsing of non-canonical IPv4 addresses. if (allowIPv6 || unknownScheme) @@ -113,8 +107,16 @@ internal static bool IsValid(ReadOnlySpan name, ref int bytesConsumed, bo // / "2" %x30-34 DIGIT ; 200-249 // / "25" %x30-35 ; 250-255 // - internal static bool IsValidCanonical(ReadOnlySpan name, ref int bytesConsumed, bool allowIPv6, bool notImplicitFile) + internal static bool IsValidCanonical(ReadOnlySpan name, ref int bytesConsumed, bool allowIPv6, bool notImplicitFile) + where TChar : unmanaged, IBinaryInteger { + TChar ComponentSeparator = TChar.CreateChecked('.'); + ReadOnlySpan PrefixSeparators = [TChar.CreateChecked('/'), TChar.CreateChecked('\\')]; + ReadOnlySpan UrlSeparators = [TChar.CreateChecked(':'), TChar.CreateChecked('?'), TChar.CreateChecked('#')]; + TChar OctalPrefix = TChar.CreateChecked('0'); + + ReadOnlySpan IPv6Terminators = [TChar.CreateChecked('%'), TChar.CreateChecked('/'), TChar.CreateChecked(']')]; + int dots = 0; int number = 0; bool haveNumber = false; @@ -129,15 +131,15 @@ internal static bool IsValidCanonical(ReadOnlySpan name, ref int bytesCon if (allowIPv6) { // for ipv4 inside ipv6 the terminator is either ScopeId, prefix or ipv6 terminator - if (ch == IPv6AddressHelper.AddressEndCharacter || ch == IPv6AddressHelper.PrefixSeparator || ch == IPv6AddressHelper.ScopeSeparator) + if (IPv6Terminators.Contains(ch)) break; } - else if (Array.IndexOf(PrefixSeparators, ch) != -1 || (notImplicitFile && (Array.IndexOf(UrlSeparators, ch) != -1))) + else if (PrefixSeparators.Contains(ch) || (notImplicitFile && UrlSeparators.Contains(ch))) { break; } - if (IPAddressParser.TryParseInteger(IPAddressParser.Decimal, ch, out int parsedCharacter)) + if (IPAddressParser.TryParseInteger(IPAddressParser.Decimal, ch, out int parsedCharacter)) { if (!haveNumber && ch == OctalPrefix) { @@ -151,7 +153,7 @@ internal static bool IsValidCanonical(ReadOnlySpan name, ref int bytesCon } haveNumber = true; - number = number * IPAddressParser.Decimal + parsedCharacter; + number = number * IPAddressParser.Decimal + parsedCharacter; if (number > byte.MaxValue) { return false; @@ -186,9 +188,16 @@ internal static bool IsValidCanonical(ReadOnlySpan name, ref int bytesCon // Return Invalid (-1) for failures. // If the address has less than three dots, only the rightmost section is assumed to contain the combined value for // the missing sections: 0xFF00FFFF == 0xFF.0x00.0xFF.0xFF == 0xFF.0xFFFF - internal static long ParseNonCanonical(ReadOnlySpan name, ref int bytesConsumed, bool notImplicitFile) + internal static long ParseNonCanonical(ReadOnlySpan name, ref int bytesConsumed, bool notImplicitFile) + where TChar : unmanaged, IBinaryInteger { - int numberBase = IPAddressParser.Decimal; + TChar ComponentSeparator = TChar.CreateChecked('.'); + ReadOnlySpan PrefixSeparators = [TChar.CreateChecked('/'), TChar.CreateChecked('\\')]; + ReadOnlySpan UrlSeparators = [TChar.CreateChecked(':'), TChar.CreateChecked('?'), TChar.CreateChecked('#')]; + ReadOnlySpan HexadecimalPrefix = [TChar.CreateChecked('0'), TChar.CreateChecked('x')]; + TChar OctalPrefix = TChar.CreateChecked('0'); + + int numberBase = IPAddressParser.Decimal; Span parts = stackalloc long[4]; long currentValue = 0; bool atLeastOneChar = false; @@ -203,10 +212,10 @@ internal static long ParseNonCanonical(ReadOnlySpan name, ref int bytesCo currentValue = 0; // Figure out what base this section is in - numberBase = IPAddressParser.Decimal; + numberBase = IPAddressParser.Decimal; if (ch == OctalPrefix) { - numberBase = IPAddressParser.Octal; + numberBase = IPAddressParser.Octal; current++; atLeastOneChar = true; if (current < name.Length) @@ -215,7 +224,7 @@ internal static long ParseNonCanonical(ReadOnlySpan name, ref int bytesCo if (ch == HexadecimalPrefix[1]) { - numberBase = IPAddressParser.Hex; + numberBase = IPAddressParser.Hex; current++; atLeastOneChar = false; } @@ -227,7 +236,7 @@ internal static long ParseNonCanonical(ReadOnlySpan name, ref int bytesCo { ch = name[current]; - if (!IPAddressParser.TryParseInteger(numberBase, ch, out int digitValue)) + if (!IPAddressParser.TryParseInteger(numberBase, ch, out int digitValue)) { break; // Invalid/terminator } @@ -269,8 +278,7 @@ internal static long ParseNonCanonical(ReadOnlySpan name, ref int bytesCo { // end of string, allowed } - else if ((!notImplicitFile && (name[current] == PrefixSeparators[0] || name[current] == PrefixSeparators[1])) - || (notImplicitFile && (name[current] == UrlSeparators[0] || name[current] == UrlSeparators[1] || name[current] == UrlSeparators[2]))) + else if (PrefixSeparators.Contains(name[current]) || (notImplicitFile && UrlSeparators.Contains(name[current]))) { bytesConsumed = current; } diff --git a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs index 6a44a503ed0ddb..a2c4c50ac19e4e 100644 --- a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs @@ -6,19 +6,8 @@ namespace System.Net { - internal static partial class IPv6AddressHelper - where TChar : unmanaged, IBinaryInteger + internal static partial class IPv6AddressHelper { - // IPv6 address-specific generic constants. - public static readonly TChar AddressStartCharacter = TChar.CreateTruncating('['); - public static readonly TChar AddressEndCharacter = TChar.CreateTruncating(']'); - public static readonly TChar ComponentSeparator = TChar.CreateTruncating(':'); - public static readonly TChar ScopeSeparator = TChar.CreateTruncating('%'); - public static readonly TChar PrefixSeparator = TChar.CreateTruncating('/'); - public static readonly TChar PortSeparator = TChar.CreateTruncating(':'); - public static readonly TChar[] HexadecimalPrefix = [TChar.CreateTruncating('0'), TChar.CreateTruncating('x')]; - public static readonly TChar[] Compressor = [ComponentSeparator, ComponentSeparator]; - private const int NumberOfLabels = 8; // RFC 5952 Section 4.2.3 @@ -105,8 +94,20 @@ internal static bool ShouldHaveIpv4Embedded(ReadOnlySpan numbers) // Remarks: MUST NOT be used unless all input indexes are verified and trusted. // start must be next to '[' position, or error is reported - internal static bool IsValidStrict(ReadOnlySpan name) + internal static bool IsValidStrict(ReadOnlySpan name) + where TChar : unmanaged, IBinaryInteger { + TChar AddressStartCharacter = TChar.CreateChecked('['); + TChar AddressEndCharacter = TChar.CreateChecked(']'); + TChar ComponentSeparator = TChar.CreateChecked(':'); + TChar ScopeSeparator = TChar.CreateChecked('%'); + TChar PrefixSeparator = TChar.CreateChecked('/'); + TChar PortSeparator = TChar.CreateChecked(':'); + ReadOnlySpan HexadecimalPrefix = [TChar.CreateChecked('0'), TChar.CreateChecked('x')]; + ReadOnlySpan Compressor = [ComponentSeparator, ComponentSeparator]; + + TChar IPv4ComponentSeparator = TChar.CreateChecked('.'); + // Number of components in this IPv6 address int sequenceCount = 0; // Length of the component currently being constructed @@ -140,7 +141,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) int i; for (i = start; i < name.Length; ++i) { - if (IPAddressParser.IsValidInteger(IPAddressParser.Hex, name[i])) + if (IPAddressParser.IsValidInteger(IPAddressParser.Hex, name[i])) { ++sequenceLength; expectingNumber = false; @@ -195,21 +196,21 @@ internal static bool IsValidStrict(ReadOnlySpan name) return false; } - int numericBase = IPAddressParser.Decimal; + int numericBase = IPAddressParser.Decimal; // Skip past the closing bracket and the port separator. i += 2; // If there is a port, it must either be a hexadecimal or decimal number. - if (i + 1 < name.Length && name.Slice(i).StartsWith(HexadecimalPrefix.AsSpan())) + if (i + 1 < name.Length && name.Slice(i).StartsWith(HexadecimalPrefix)) { i += HexadecimalPrefix.Length; - numericBase = IPAddressParser.Hex; + numericBase = IPAddressParser.Hex; } for (; i < name.Length; i++) { - if (!IPAddressParser.IsValidInteger(numericBase, name[i])) + if (!IPAddressParser.IsValidInteger(numericBase, name[i])) { return false; } @@ -222,7 +223,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) } else if (name[i] == ComponentSeparator) { - if (i > 0 && name.Slice(i - 1, 2).SequenceEqual(Compressor.AsSpan())) + if (i > 0 && name.Slice(i - 1, 2).SequenceEqual(Compressor)) { if (haveCompressor) { @@ -240,7 +241,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) sequenceLength = 0; continue; } - else if (name[i] == IPv4AddressHelper.ComponentSeparator) + else if (name[i] == IPv4ComponentSeparator) { int ipv4AddressLength = i; @@ -249,7 +250,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) return false; } - if (!IPv4AddressHelper.IsValid(name.Slice(lastSequence), ref ipv4AddressLength, true, false, false)) + if (!IPv4AddressHelper.IsValid(name.Slice(lastSequence), ref ipv4AddressLength, true, false, false)) { return false; } @@ -312,8 +313,18 @@ internal static bool IsValidStrict(ReadOnlySpan name) // Nothing // - internal static void Parse(ReadOnlySpan address, scoped Span numbers, out ReadOnlySpan scopeId) + internal static void Parse(ReadOnlySpan address, scoped Span numbers, out ReadOnlySpan scopeId) + where TChar : unmanaged, IBinaryInteger { + TChar AddressStartCharacter = TChar.CreateChecked('['); + TChar AddressEndCharacter = TChar.CreateChecked(']'); + TChar ComponentSeparator = TChar.CreateChecked(':'); + TChar ScopeSeparator = TChar.CreateChecked('%'); + TChar PrefixSeparator = TChar.CreateChecked('/'); + ReadOnlySpan Compressor = [ComponentSeparator, ComponentSeparator]; + + TChar IPv4ComponentSeparator = TChar.CreateChecked('.'); + int number = 0; int index = 0; int compressorIndex = -1; @@ -373,7 +384,7 @@ internal static void Parse(ReadOnlySpan address, scoped Span numb (j < i + 4); ++j) { - if (address[j] == IPv4AddressHelper.ComponentSeparator) + if (address[j] == IPv4ComponentSeparator) { // we have an IPv4 address. Find the end of it: // we know that since we have a valid IPv6 @@ -385,7 +396,7 @@ internal static void Parse(ReadOnlySpan address, scoped Span numb { ++j; } - number = IPv4AddressHelper.ParseHostNumber(address.Slice(i, j - i)); + number = IPv4AddressHelper.ParseHostNumber(address.Slice(i, j - i)); numbers[index++] = (ushort)(number >> 16); numbers[index++] = (ushort)number; i = j; @@ -400,7 +411,7 @@ internal static void Parse(ReadOnlySpan address, scoped Span numb } else { - number = number * IPAddressParser.Hex + Uri.FromHex((char)short.CreateChecked(address[i++])); + number = number * IPAddressParser.Hex + Uri.FromHex((char)short.CreateChecked(address[i++])); } } diff --git a/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs b/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs index 12b21d181135e0..cb44783ca0b3b3 100644 --- a/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs +++ b/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs @@ -55,16 +55,16 @@ internal static unsafe bool IsValidAddress(string? hostname) // we don't support/parse a port specification at the end of an IPv4 address. Span numbers = stackalloc ushort[IPAddressParserStatics.IPv6AddressShorts]; - return IPv6AddressHelper.IsValidStrict(ipSpan); + return IPv6AddressHelper.IsValidStrict(ipSpan); } else if (char.IsDigit(ipSpan[0])) { long tmpAddr; int end = ipSpan.Length; - tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipSpan, ref end, notImplicitFile: true); + tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipSpan, ref end, notImplicitFile: true); - if (tmpAddr != IPv4AddressHelper.Invalid && end == ipSpan.Length) + if (tmpAddr != IPv4AddressHelper.Invalid && end == ipSpan.Length) { return true; } diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs index afac03a667100c..8a6288c2f409ca 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs @@ -228,7 +228,7 @@ public static bool TryParse([NotNullWhen(true)] string? ipString, [NotNullWhen(t return false; } - address = IPAddressParser.Parse(ipString.AsSpan(), tryParse: true); + address = IPAddressParser.Parse(ipString.AsSpan(), tryParse: true); return (address != null); } @@ -240,13 +240,13 @@ public static bool TryParse([NotNullWhen(true)] string? ipString, [NotNullWhen(t /// true if was successfully parsed; otherwise, false. public static bool TryParse(ReadOnlySpan utf8Text, [NotNullWhen(true)] out IPAddress? result) { - result = IPAddressParser.Parse(utf8Text, tryParse: true); + result = IPAddressParser.Parse(utf8Text, tryParse: true); return (result != null); } public static bool TryParse(ReadOnlySpan ipSpan, [NotNullWhen(true)] out IPAddress? address) { - address = IPAddressParser.Parse(ipSpan, tryParse: true); + address = IPAddressParser.Parse(ipSpan, tryParse: true); return (address != null); } @@ -268,7 +268,7 @@ public static IPAddress Parse(string ipString) { ArgumentNullException.ThrowIfNull(ipString); - return IPAddressParser.Parse(ipString.AsSpan(), tryParse: false)!; + return IPAddressParser.Parse(ipString.AsSpan(), tryParse: false)!; } /// @@ -278,12 +278,12 @@ public static IPAddress Parse(string ipString) /// The result of parsing . public static IPAddress Parse(ReadOnlySpan utf8Text) { - return IPAddressParser.Parse(utf8Text, tryParse: false)!; + return IPAddressParser.Parse(utf8Text, tryParse: false)!; } public static IPAddress Parse(ReadOnlySpan ipSpan) { - return IPAddressParser.Parse(ipSpan, tryParse: false)!; + return IPAddressParser.Parse(ipSpan, tryParse: false)!; } /// @@ -439,10 +439,10 @@ public override string ToString() string? toString = _toString; if (toString is null) { - Span span = stackalloc char[IPAddressParser.MaxIPv6StringLength]; + Span span = stackalloc char[IPAddressParser.MaxIPv6StringLength]; int length = IsIPv4 ? - IPAddressParser.FormatIPv4Address(_addressOrScopeId, span) : - IPAddressParser.FormatIPv6Address(_numbers, _addressOrScopeId, span); + IPAddressParser.FormatIPv4Address(_addressOrScopeId, span) : + IPAddressParser.FormatIPv6Address(_numbers, _addressOrScopeId, span); _toString = toString = new string(span.Slice(0, length)); } @@ -478,27 +478,27 @@ private bool TryFormatCore(Span destination, out int charsWritten) { if (IsIPv4) { - if (destination.Length >= IPAddressParser.MaxIPv4StringLength) + if (destination.Length >= IPAddressParser.MaxIPv4StringLength) { - charsWritten = IPAddressParser.FormatIPv4Address(_addressOrScopeId, destination); + charsWritten = IPAddressParser.FormatIPv4Address(_addressOrScopeId, destination); return true; } } else { - if (destination.Length >= IPAddressParser.MaxIPv6StringLength) + if (destination.Length >= IPAddressParser.MaxIPv6StringLength) { - charsWritten = IPAddressParser.FormatIPv6Address(_numbers, _addressOrScopeId, destination); + charsWritten = IPAddressParser.FormatIPv6Address(_numbers, _addressOrScopeId, destination); return true; } } - Span tmpDestination = stackalloc TChar[IPAddressParser.MaxIPv6StringLength]; - Debug.Assert(tmpDestination.Length >= IPAddressParser.MaxIPv4StringLength); + Span tmpDestination = stackalloc TChar[IPAddressParser.MaxIPv6StringLength]; + Debug.Assert(tmpDestination.Length >= IPAddressParser.MaxIPv4StringLength); int written = IsIPv4 ? - IPAddressParser.FormatIPv4Address(PrivateAddress, tmpDestination) : - IPAddressParser.FormatIPv6Address(_numbers, PrivateScopeId, tmpDestination); + IPAddressParser.FormatIPv4Address(PrivateAddress, tmpDestination) : + IPAddressParser.FormatIPv6Address(_numbers, PrivateScopeId, tmpDestination); if (tmpDestination.Slice(0, written).TryCopyTo(destination)) { diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs index b3a61bdaaad1db..0a6859ceb23785 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs @@ -5,18 +5,22 @@ using System.Globalization; using System.Net.NetworkInformation; using System.Net.Sockets; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace System.Net { - internal static partial class IPAddressParser + internal static partial class IPAddressParser { - internal static IPAddress? Parse(ReadOnlySpan ipSpan, bool tryParse) + internal static IPAddress? Parse(ReadOnlySpan ipSpan, bool tryParse) + where TChar : unmanaged, IBinaryInteger { + TChar IPv6ComponentSeparator = TChar.CreateChecked(':'); + Debug.Assert(typeof(TChar) == typeof(byte) || typeof(TChar) == typeof(char)); - if (ipSpan.Contains(IPv6AddressHelper.ComponentSeparator)) + if (ipSpan.Contains(IPv6ComponentSeparator)) { // The address is parsed as IPv6 if and only if it contains a colon. This is valid because // we don't support/parse a port specification at the end of an IPv4 address. @@ -40,14 +44,15 @@ internal static partial class IPAddressParser throw new FormatException(SR.dns_bad_ip_address, new SocketException(SocketError.InvalidArgument)); } - private static bool TryParseIpv4(ReadOnlySpan ipSpan, out long address) + private static bool TryParseIpv4(ReadOnlySpan ipSpan, out long address) + where TChar : unmanaged, IBinaryInteger { int end = ipSpan.Length; long tmpAddr; - tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipSpan, ref end, notImplicitFile: true); + tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipSpan, ref end, notImplicitFile: true); - if (tmpAddr != IPv4AddressHelper.Invalid && end == ipSpan.Length) + if (tmpAddr != IPv4AddressHelper.Invalid && end == ipSpan.Length) { // IPv4AddressHelper.ParseNonCanonical returns the bytes in host order. // Convert to network order and return success. @@ -60,17 +65,18 @@ private static bool TryParseIpv4(ReadOnlySpan ipSpan, out long address) return false; } - private static bool TryParseIPv6(ReadOnlySpan ipSpan, Span numbers, int numbersLength, out uint scope) + private static bool TryParseIPv6(ReadOnlySpan ipSpan, Span numbers, int numbersLength, out uint scope) + where TChar : unmanaged, IBinaryInteger { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); Debug.Assert(numbersLength >= IPAddressParserStatics.IPv6AddressShorts); - bool isValid = IPv6AddressHelper.IsValidStrict(ipSpan); + bool isValid = IPv6AddressHelper.IsValidStrict(ipSpan); scope = 0; if (isValid) { - IPv6AddressHelper.Parse(ipSpan, numbers, out ReadOnlySpan scopeIdSpan); + IPv6AddressHelper.Parse(ipSpan, numbers, out ReadOnlySpan scopeIdSpan); if (scopeIdSpan.Length > 1) { @@ -119,7 +125,8 @@ private static bool TryParseIPv6(ReadOnlySpan ipSpan, Span number return false; } - internal static int FormatIPv4Address(uint address, Span addressString) + internal static int FormatIPv4Address(uint address, Span addressString) + where TChar : unmanaged, IBinaryInteger { address = (uint)IPAddress.NetworkToHostOrder(unchecked((int)address)); @@ -163,11 +170,12 @@ static int FormatByte(uint number, Span addressString) } } - internal static int FormatIPv6Address(ushort[] address, uint scopeId, Span destination) + internal static int FormatIPv6Address(ushort[] address, uint scopeId, Span destination) + where TChar : unmanaged, IBinaryInteger { int pos = 0; - if (IPv6AddressHelper.ShouldHaveIpv4Embedded(address)) + if (IPv6AddressHelper.ShouldHaveIpv4Embedded(address)) { // We need to treat the last 2 ushorts as a 4-byte IPv4 address, // so output the first 6 ushorts normally, followed by the IPv4 address. @@ -213,7 +221,7 @@ internal static int FormatIPv6Address(ushort[] address, uint scopeId, Span address, Span destination, ref int offset) { // Find the longest sequence of zeros to be combined into a "::" - (int zeroStart, int zeroEnd) = IPv6AddressHelper.FindCompressionRange(address); + (int zeroStart, int zeroEnd) = IPv6AddressHelper.FindCompressionRange(address); bool needsColon = false; // Handle a zero sequence if there is one diff --git a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs index 0e532c550cd341..3de779fb9d8298 100644 --- a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs +++ b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs @@ -5,10 +5,11 @@ namespace System.Net { - internal static class IPv4AddressHelper - where TChar : unmanaged, IBinaryInteger + internal static class IPv4AddressHelper { internal const int Invalid = -1; - internal static unsafe long ParseNonCanonical(ReadOnlySpan name, ref int bytesConsumed, bool notImplicitFile) => 0; + internal static unsafe long ParseNonCanonical(ReadOnlySpan name, ref int bytesConsumed, bool notImplicitFile) + where TChar : unmanaged, IBinaryInteger + => 0; } } diff --git a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs index daa43df211c366..37262b6471b30c 100644 --- a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs +++ b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs @@ -6,16 +6,16 @@ namespace System.Net { - internal static class IPv6AddressHelper - where TChar : unmanaged, IBinaryInteger + internal static class IPv6AddressHelper { - public static readonly TChar ComponentSeparator = TChar.CreateTruncating(':'); - internal static unsafe (int longestSequenceStart, int longestSequenceLength) FindCompressionRange( ReadOnlySpan numbers) => (-1, -1); internal static unsafe bool ShouldHaveIpv4Embedded(ReadOnlySpan numbers) => false; - internal static unsafe bool IsValidStrict(ReadOnlySpan name) => false; - internal static unsafe bool Parse(ReadOnlySpan address, Span numbers, out ReadOnlySpan scopeId) + internal static unsafe bool IsValidStrict(ReadOnlySpan name) + where TChar : unmanaged, IBinaryInteger + => false; + internal static unsafe bool Parse(ReadOnlySpan address, Span numbers, out ReadOnlySpan scopeId) + where TChar : unmanaged, IBinaryInteger { scopeId = ReadOnlySpan.Empty; return false; diff --git a/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs b/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs index 05ed93e012eb4b..45fedda6748295 100644 --- a/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs +++ b/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs @@ -2,16 +2,18 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Numerics; namespace System.Net { // The class designed as to keep minimal the working set of Uri class. // The idea is to stay with static helper methods and strings - internal static partial class IPv4AddressHelper + internal static partial class IPv4AddressHelper { // methods // Parse and canonicalize - internal static string ParseCanonicalName(ReadOnlySpan str, ref bool isLoopback) + internal static string ParseCanonicalName(ReadOnlySpan str, ref bool isLoopback) + where TChar : unmanaged, IBinaryInteger { Span numbers = stackalloc byte[NumberOfLabels]; isLoopback = Parse(str, numbers); @@ -34,7 +36,8 @@ internal static string ParseCanonicalName(ReadOnlySpan str, ref bool isLo // // Convert this IPv4 address into a sequence of 4 8-bit numbers // - private static bool Parse(ReadOnlySpan name, Span numbers) + private static bool Parse(ReadOnlySpan name, Span numbers) + where TChar : unmanaged, IBinaryInteger { // "name" parameter includes ports, so bytesConsumed may be different from span length int bytesConsumed = 0; diff --git a/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs b/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs index bc0107c6bc014e..63c0fae7340b7d 100644 --- a/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs +++ b/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs @@ -2,14 +2,16 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Numerics; namespace System.Net { // The class designed as to keep minimal the working set of Uri class. // The idea is to stay with static helper methods and strings - internal static partial class IPv6AddressHelper + internal static partial class IPv6AddressHelper { - internal static string ParseCanonicalName(ReadOnlySpan str, ref bool isLoopback, out ReadOnlySpan scopeId) + internal static string ParseCanonicalName(ReadOnlySpan str, ref bool isLoopback, out ReadOnlySpan scopeId) + where TChar : unmanaged, IBinaryInteger { Span numbers = stackalloc ushort[NumberOfLabels]; numbers.Clear(); @@ -245,7 +247,7 @@ private static unsafe bool InternalIsValid(char* name, int start, ref int end, b return false; } - if (!IPv4AddressHelper.IsValid(new ReadOnlySpan(name + lastSequence, end - lastSequence), ref ipv4AddressLength, true, false, false)) + if (!IPv4AddressHelper.IsValid(new ReadOnlySpan(name + lastSequence, end - lastSequence), ref ipv4AddressLength, true, false, false)) { return false; } diff --git a/src/libraries/System.Private.Uri/src/System/Uri.cs b/src/libraries/System.Private.Uri/src/System/Uri.cs index 22f7c8e0e81911..f3187bbe78e3dd 100644 --- a/src/libraries/System.Private.Uri/src/System/Uri.cs +++ b/src/libraries/System.Private.Uri/src/System/Uri.cs @@ -1276,7 +1276,7 @@ public static UriHostNameType CheckHostName(string? name) if (name.StartsWith('[') && name.EndsWith(']')) { // we require that _entire_ name is recognized as ipv6 address - if (IPv6AddressHelper.IsValid(fixedName, 1, ref end) && end == name.Length) + if (IPv6AddressHelper.IsValid(fixedName, 1, ref end) && end == name.Length) { return UriHostNameType.IPv6; } @@ -1284,7 +1284,7 @@ public static UriHostNameType CheckHostName(string? name) } end = name.Length; - if (IPv4AddressHelper.IsValid(name.AsSpan(), ref end, false, false, false) && end == name.Length) + if (IPv4AddressHelper.IsValid(name.AsSpan(), ref end, false, false, false) && end == name.Length) { return UriHostNameType.IPv4; } @@ -1305,7 +1305,7 @@ public static UriHostNameType CheckHostName(string? name) name = "[" + name + "]"; fixed (char* newFixedName = name) { - if (IPv6AddressHelper.IsValid(newFixedName, 1, ref end) && end == name.Length) + if (IPv6AddressHelper.IsValid(newFixedName, 1, ref end) && end == name.Length) { return UriHostNameType.IPv6; } @@ -2515,12 +2515,12 @@ private static string CreateHostStringHelper(string str, int idx, int end, ref F case Flags.IPv6HostType: // The helper will return [...] string that is not suited for Dns.Resolve() - host = IPv6AddressHelper.ParseCanonicalName(str.AsSpan(idx), ref loopback, out ReadOnlySpan scopeIdSpan); + host = IPv6AddressHelper.ParseCanonicalName(str.AsSpan(idx), ref loopback, out ReadOnlySpan scopeIdSpan); scopeId = scopeIdSpan.IsEmpty ? null : new string(scopeIdSpan); break; case Flags.IPv4HostType: - host = IPv4AddressHelper.ParseCanonicalName(str.AsSpan(idx), ref loopback); + host = IPv4AddressHelper.ParseCanonicalName(str.AsSpan(idx), ref loopback); break; case Flags.UncHostType: @@ -3854,7 +3854,7 @@ private unsafe int CheckAuthorityHelper(char* pString, int idx, int length, int bytesConsumed = 0; if (ch == '[' && syntax.InFact(UriSyntaxFlags.AllowIPv6Host) && - IPv6AddressHelper.IsValid(pString, start + 1, ref end)) + IPv6AddressHelper.IsValid(pString, start + 1, ref end)) { flags |= Flags.IPv6HostType; @@ -3864,7 +3864,7 @@ private unsafe int CheckAuthorityHelper(char* pString, int idx, int length, } } else if (char.IsAsciiDigit(ch) && syntax.InFact(UriSyntaxFlags.AllowIPv4Host) && - IPv4AddressHelper.IsValid(new ReadOnlySpan(pString + start, end - start), ref bytesConsumed, false, StaticNotAny(flags, Flags.ImplicitFile), syntax.InFact(UriSyntaxFlags.V1_UnknownUri))) + IPv4AddressHelper.IsValid(new ReadOnlySpan(pString + start, end - start), ref bytesConsumed, false, StaticNotAny(flags, Flags.ImplicitFile), syntax.InFact(UriSyntaxFlags.V1_UnknownUri))) { end = start + bytesConsumed; flags |= Flags.IPv4HostType; From 49bc3a52e2f538efc3209f83d5be35b6e73b89f6 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 22 Jun 2024 15:28:16 +0100 Subject: [PATCH 15/43] Replaced ref parameter with out parameter, propagated Also made one adjustment in Uri so that it uses BinaryPrimitives to convert a long to a Span, rather than manual bit shifts. --- .../src/System/Net/IPv4AddressHelper.Common.cs | 13 ++++++++----- .../src/System/Net/IPv6AddressHelper.Common.cs | 4 +--- .../src/System/Net/Security/TargetHostNameHelper.cs | 7 ++----- .../src/System/Net/IPAddressParser.cs | 3 +-- .../tests/UnitTests/Fakes/IPv4AddressHelper.cs | 7 +++++-- .../src/System/IPv4AddressHelper.cs | 11 ++--------- .../src/System/IPv6AddressHelper.cs | 4 +--- src/libraries/System.Private.Uri/src/System/Uri.cs | 7 ++----- 8 files changed, 22 insertions(+), 34 deletions(-) diff --git a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs index 1ef8f45c22ebcd..48e40d2ce9540e 100644 --- a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs @@ -80,17 +80,17 @@ internal static int ParseHostNumber(ReadOnlySpan str) // //Remark: MUST NOT be used unless all input indexes are verified and trusted. - internal static bool IsValid(ReadOnlySpan name, ref int bytesConsumed, bool allowIPv6, bool notImplicitFile, bool unknownScheme) + internal static bool IsValid(ReadOnlySpan name, out int bytesConsumed, bool allowIPv6, bool notImplicitFile, bool unknownScheme) where TChar : unmanaged, IBinaryInteger { // IPv6 can only have canonical IPv4 embedded. Unknown schemes will not attempt parsing of non-canonical IPv4 addresses. if (allowIPv6 || unknownScheme) { - return IsValidCanonical(name, ref bytesConsumed, allowIPv6, notImplicitFile); + return IsValidCanonical(name, out bytesConsumed, allowIPv6, notImplicitFile); } else { - return ParseNonCanonical(name, ref bytesConsumed, notImplicitFile) != Invalid; + return ParseNonCanonical(name, out bytesConsumed, notImplicitFile) != Invalid; } } @@ -107,7 +107,7 @@ internal static bool IsValid(ReadOnlySpan name, ref int bytesConsu // / "2" %x30-34 DIGIT ; 200-249 // / "25" %x30-35 ; 250-255 // - internal static bool IsValidCanonical(ReadOnlySpan name, ref int bytesConsumed, bool allowIPv6, bool notImplicitFile) + internal static bool IsValidCanonical(ReadOnlySpan name, out int bytesConsumed, bool allowIPv6, bool notImplicitFile) where TChar : unmanaged, IBinaryInteger { TChar ComponentSeparator = TChar.CreateChecked('.'); @@ -124,6 +124,8 @@ internal static bool IsValidCanonical(ReadOnlySpan name, ref int b int current; Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); + + bytesConsumed = 0; for (current = 0; current < name.Length; current++) { TChar ch = name[current]; @@ -188,7 +190,7 @@ internal static bool IsValidCanonical(ReadOnlySpan name, ref int b // Return Invalid (-1) for failures. // If the address has less than three dots, only the rightmost section is assumed to contain the combined value for // the missing sections: 0xFF00FFFF == 0xFF.0x00.0xFF.0xFF == 0xFF.0xFFFF - internal static long ParseNonCanonical(ReadOnlySpan name, ref int bytesConsumed, bool notImplicitFile) + internal static long ParseNonCanonical(ReadOnlySpan name, out int bytesConsumed, bool notImplicitFile) where TChar : unmanaged, IBinaryInteger { TChar ComponentSeparator = TChar.CreateChecked('.'); @@ -206,6 +208,7 @@ internal static long ParseNonCanonical(ReadOnlySpan name, ref int int dotCount = 0; // Limit 3 int current; + bytesConsumed = 0; for (current = 0; current < name.Length; current++) { TChar ch = name[current]; diff --git a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs index a2c4c50ac19e4e..99468b2dbb3fa8 100644 --- a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs @@ -243,14 +243,12 @@ internal static bool IsValidStrict(ReadOnlySpan name) } else if (name[i] == IPv4ComponentSeparator) { - int ipv4AddressLength = i; - if (haveIPv4Address) { return false; } - if (!IPv4AddressHelper.IsValid(name.Slice(lastSequence), ref ipv4AddressLength, true, false, false)) + if (!IPv4AddressHelper.IsValid(name.Slice(lastSequence), out int ipv4AddressLength, true, false, false)) { return false; } diff --git a/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs b/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs index cb44783ca0b3b3..0ac5e9e8542eec 100644 --- a/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs +++ b/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs @@ -40,7 +40,7 @@ internal static string NormalizeHostName(string? targetHost) // Simplified version of IPAddressParser.Parse to avoid allocations and dependencies. // It purposely ignores scopeId as we don't really use so we do not need to map it to actual interface id. - internal static unsafe bool IsValidAddress(string? hostname) + internal static bool IsValidAddress(string? hostname) { if (string.IsNullOrEmpty(hostname)) { @@ -59,10 +59,7 @@ internal static unsafe bool IsValidAddress(string? hostname) } else if (char.IsDigit(ipSpan[0])) { - long tmpAddr; - int end = ipSpan.Length; - - tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipSpan, ref end, notImplicitFile: true); + long tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipSpan, out int end, notImplicitFile: true); if (tmpAddr != IPv4AddressHelper.Invalid && end == ipSpan.Length) { diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs index 0a6859ceb23785..8187ff093706ac 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs @@ -47,10 +47,9 @@ internal static partial class IPAddressParser private static bool TryParseIpv4(ReadOnlySpan ipSpan, out long address) where TChar : unmanaged, IBinaryInteger { - int end = ipSpan.Length; long tmpAddr; - tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipSpan, ref end, notImplicitFile: true); + tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipSpan, out int end, notImplicitFile: true); if (tmpAddr != IPv4AddressHelper.Invalid && end == ipSpan.Length) { diff --git a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs index 3de779fb9d8298..0810a29d6589f4 100644 --- a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs +++ b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs @@ -8,8 +8,11 @@ namespace System.Net internal static class IPv4AddressHelper { internal const int Invalid = -1; - internal static unsafe long ParseNonCanonical(ReadOnlySpan name, ref int bytesConsumed, bool notImplicitFile) + internal static unsafe long ParseNonCanonical(ReadOnlySpan name, out int bytesConsumed, bool notImplicitFile) where TChar : unmanaged, IBinaryInteger - => 0; + { + bytesConsumed = 0; + return 0; + } } } diff --git a/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs b/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs index 45fedda6748295..f4f0f04110ead8 100644 --- a/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs +++ b/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs @@ -40,18 +40,11 @@ private static bool Parse(ReadOnlySpan name, Span numbers) where TChar : unmanaged, IBinaryInteger { // "name" parameter includes ports, so bytesConsumed may be different from span length - int bytesConsumed = 0; - long result = ParseNonCanonical(name, ref bytesConsumed, true); + long result = ParseNonCanonical(name, out _, true); Debug.Assert(result != Invalid, $"Failed to parse after already validated: {string.Join(string.Empty, name.ToArray())}"); - unchecked - { - numbers[0] = (byte)(result >> 24); - numbers[1] = (byte)(result >> 16); - numbers[2] = (byte)(result >> 8); - numbers[3] = (byte)(result); - } + System.Buffers.Binary.BinaryPrimitives.WriteUInt32BigEndian(numbers, (uint)result); return numbers[0] == 127; } diff --git a/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs b/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs index 63c0fae7340b7d..3c2864d63fd039 100644 --- a/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs +++ b/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs @@ -240,14 +240,12 @@ private static unsafe bool InternalIsValid(char* name, int start, ref int end, b break; case '.': - int ipv4AddressLength = i; - if (haveIPv4Address) { return false; } - if (!IPv4AddressHelper.IsValid(new ReadOnlySpan(name + lastSequence, end - lastSequence), ref ipv4AddressLength, true, false, false)) + if (!IPv4AddressHelper.IsValid(new ReadOnlySpan(name + lastSequence, end - lastSequence), out int ipv4AddressLength, true, false, false)) { return false; } diff --git a/src/libraries/System.Private.Uri/src/System/Uri.cs b/src/libraries/System.Private.Uri/src/System/Uri.cs index f3187bbe78e3dd..32e7c8a0620f76 100644 --- a/src/libraries/System.Private.Uri/src/System/Uri.cs +++ b/src/libraries/System.Private.Uri/src/System/Uri.cs @@ -1283,8 +1283,7 @@ public static UriHostNameType CheckHostName(string? name) } } - end = name.Length; - if (IPv4AddressHelper.IsValid(name.AsSpan(), ref end, false, false, false) && end == name.Length) + if (IPv4AddressHelper.IsValid(name.AsSpan(), out end, false, false, false) && end == name.Length) { return UriHostNameType.IPv4; } @@ -3851,8 +3850,6 @@ private unsafe int CheckAuthorityHelper(char* pString, int idx, int length, } } - int bytesConsumed = 0; - if (ch == '[' && syntax.InFact(UriSyntaxFlags.AllowIPv6Host) && IPv6AddressHelper.IsValid(pString, start + 1, ref end)) { @@ -3864,7 +3861,7 @@ private unsafe int CheckAuthorityHelper(char* pString, int idx, int length, } } else if (char.IsAsciiDigit(ch) && syntax.InFact(UriSyntaxFlags.AllowIPv4Host) && - IPv4AddressHelper.IsValid(new ReadOnlySpan(pString + start, end - start), ref bytesConsumed, false, StaticNotAny(flags, Flags.ImplicitFile), syntax.InFact(UriSyntaxFlags.V1_UnknownUri))) + IPv4AddressHelper.IsValid(new ReadOnlySpan(pString + start, end - start), out int bytesConsumed, false, StaticNotAny(flags, Flags.ImplicitFile), syntax.InFact(UriSyntaxFlags.V1_UnknownUri))) { end = start + bytesConsumed; flags |= Flags.IPv4HostType; From 3378947b2d3bf42a772a17b0bf7d52b828ca25ff Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sun, 23 Jun 2024 13:01:27 +0100 Subject: [PATCH 16/43] Addressing @jkotas code review comments --- .../src/System/Net/IPv4AddressHelper.Common.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs index 48e40d2ce9540e..4cf0b7d396cdbb 100644 --- a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs @@ -133,10 +133,13 @@ internal static bool IsValidCanonical(ReadOnlySpan name, out int b if (allowIPv6) { // for ipv4 inside ipv6 the terminator is either ScopeId, prefix or ipv6 terminator - if (IPv6Terminators.Contains(ch)) + if (ch == IPv6Terminators[0] || ch == IPv6Terminators[1] || ch == IPv6Terminators[2]) + { break; + } } - else if (PrefixSeparators.Contains(ch) || (notImplicitFile && UrlSeparators.Contains(ch))) + else if (ch == PrefixSeparators[0] || ch == PrefixSeparators[1] + || (notImplicitFile && (ch == UrlSeparators[0] || ch == UrlSeparators[1] || ch == UrlSeparators[2]))) { break; } @@ -196,8 +199,8 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int TChar ComponentSeparator = TChar.CreateChecked('.'); ReadOnlySpan PrefixSeparators = [TChar.CreateChecked('/'), TChar.CreateChecked('\\')]; ReadOnlySpan UrlSeparators = [TChar.CreateChecked(':'), TChar.CreateChecked('?'), TChar.CreateChecked('#')]; - ReadOnlySpan HexadecimalPrefix = [TChar.CreateChecked('0'), TChar.CreateChecked('x')]; TChar OctalPrefix = TChar.CreateChecked('0'); + ReadOnlySpan HexadecimalPrefix = [OctalPrefix, TChar.CreateChecked('x')]; int numberBase = IPAddressParser.Decimal; Span parts = stackalloc long[4]; @@ -281,7 +284,8 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int { // end of string, allowed } - else if (PrefixSeparators.Contains(name[current]) || (notImplicitFile && UrlSeparators.Contains(name[current]))) + else if (name[current] == PrefixSeparators[0] || name[current] == PrefixSeparators[1] + || (notImplicitFile && (name[current] == UrlSeparators[0] || name[current] == UrlSeparators[1] || name[current] == UrlSeparators[2]))) { bytesConsumed = current; } From 66ec8c259e68d4a63f514557f34d3952774a6346 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sun, 23 Jun 2024 17:35:14 +0100 Subject: [PATCH 17/43] Inlined all constant-value variables --- .../System/Net/IPv4AddressHelper.Common.cs | 64 ++++++------- .../System/Net/IPv6AddressHelper.Common.cs | 94 +++++++++---------- .../src/System/Net/IPAddressParser.cs | 4 +- 3 files changed, 72 insertions(+), 90 deletions(-) diff --git a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs index 4cf0b7d396cdbb..63db77f5d205df 100644 --- a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs @@ -48,16 +48,6 @@ internal static int ParseHostNumber(ReadOnlySpan str) // name // string containing possible IPv4 address // - // start - // offset in to start checking for IPv4 address - // - // end - // offset in of the last character we can touch in the check - // - // Outputs: - // end - // index of last character in we checked - // // allowIPv6 // enables parsing IPv4 addresses embedded in IPv6 address literals // @@ -67,6 +57,10 @@ internal static int ParseHostNumber(ReadOnlySpan str) // unknownScheme // the check is made on an unknown scheme (suppress IPv4 canonicalization) // + // Outputs: + // bytesConsumed + // index of last character in we checked + // // Assumes: // The address string is terminated by either // end of the string, characters ':' '/' '\' '?' @@ -110,13 +104,6 @@ internal static bool IsValid(ReadOnlySpan name, out int bytesConsu internal static bool IsValidCanonical(ReadOnlySpan name, out int bytesConsumed, bool allowIPv6, bool notImplicitFile) where TChar : unmanaged, IBinaryInteger { - TChar ComponentSeparator = TChar.CreateChecked('.'); - ReadOnlySpan PrefixSeparators = [TChar.CreateChecked('/'), TChar.CreateChecked('\\')]; - ReadOnlySpan UrlSeparators = [TChar.CreateChecked(':'), TChar.CreateChecked('?'), TChar.CreateChecked('#')]; - TChar OctalPrefix = TChar.CreateChecked('0'); - - ReadOnlySpan IPv6Terminators = [TChar.CreateChecked('%'), TChar.CreateChecked('/'), TChar.CreateChecked(']')]; - int dots = 0; int number = 0; bool haveNumber = false; @@ -132,23 +119,26 @@ internal static bool IsValidCanonical(ReadOnlySpan name, out int b if (allowIPv6) { - // for ipv4 inside ipv6 the terminator is either ScopeId, prefix or ipv6 terminator - if (ch == IPv6Terminators[0] || ch == IPv6Terminators[1] || ch == IPv6Terminators[2]) + // For an IPv4 address nested inside an IPv6, the terminator is either ScopeId ('%'), prefix ('/') or ipv6 address terminator (']') + if (ch == TChar.CreateChecked('%') || ch == TChar.CreateChecked('/') || ch == TChar.CreateChecked(']')) { break; } } - else if (ch == PrefixSeparators[0] || ch == PrefixSeparators[1] - || (notImplicitFile && (ch == UrlSeparators[0] || ch == UrlSeparators[1] || ch == UrlSeparators[2]))) + // For a normal IPv4 address, the terminator is the prefix ('/' or its counterpart, '\'). If notImplicitFile is set, the terminator + // is one of the characters which signify the start of the rest of the URI - the port number (':'), query string ('?') or fragment ('#') + else if (ch == TChar.CreateChecked('/') || ch == TChar.CreateChecked('\\') + || (notImplicitFile && (ch == TChar.CreateChecked(':') || ch == TChar.CreateChecked('?') || ch == TChar.CreateChecked('#')))) { break; } if (IPAddressParser.TryParseInteger(IPAddressParser.Decimal, ch, out int parsedCharacter)) { - if (!haveNumber && ch == OctalPrefix) + // A number starting with zero should be interpreted in base 8 / octal + if (!haveNumber && ch == TChar.CreateChecked('0')) { - if (current + 1 < name.Length && name[current + 1] == OctalPrefix) + if (current + 1 < name.Length && name[current + 1] == TChar.CreateChecked('0')) { // 00 is not allowed as a prefix. return false; @@ -164,7 +154,8 @@ internal static bool IsValidCanonical(ReadOnlySpan name, out int b return false; } } - else if (ch == ComponentSeparator) + // If the current character is not an integer, it may be the IPv4 component separator ('.') + else if (ch == TChar.CreateChecked('.')) { if (!haveNumber || (number > 0 && firstCharIsZero)) { @@ -196,12 +187,6 @@ internal static bool IsValidCanonical(ReadOnlySpan name, out int b internal static long ParseNonCanonical(ReadOnlySpan name, out int bytesConsumed, bool notImplicitFile) where TChar : unmanaged, IBinaryInteger { - TChar ComponentSeparator = TChar.CreateChecked('.'); - ReadOnlySpan PrefixSeparators = [TChar.CreateChecked('/'), TChar.CreateChecked('\\')]; - ReadOnlySpan UrlSeparators = [TChar.CreateChecked(':'), TChar.CreateChecked('?'), TChar.CreateChecked('#')]; - TChar OctalPrefix = TChar.CreateChecked('0'); - ReadOnlySpan HexadecimalPrefix = [OctalPrefix, TChar.CreateChecked('x')]; - int numberBase = IPAddressParser.Decimal; Span parts = stackalloc long[4]; long currentValue = 0; @@ -217,18 +202,21 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int TChar ch = name[current]; currentValue = 0; - // Figure out what base this section is in + // Figure out what base this section is in, default to base 10 numberBase = IPAddressParser.Decimal; - if (ch == OctalPrefix) + // A number starting with zero should be interpreted in base 8 / octal + // If the number starts with 0x, it should be interpreted in base 16 / hex + if (ch == TChar.CreateChecked('0')) { numberBase = IPAddressParser.Octal; current++; atLeastOneChar = true; if (current < name.Length) { - ch = name[current] | TChar.CreateTruncating(0x20); + // Force an uppercase 'X' to lowercase 'x'. + ch = name[current] | TChar.CreateChecked(0x20); - if (ch == HexadecimalPrefix[1]) + if (ch == TChar.CreateChecked('x')) { numberBase = IPAddressParser.Hex; current++; @@ -257,7 +245,7 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int atLeastOneChar = true; } - if (current < name.Length && name[current] == ComponentSeparator) + if (current < name.Length && name[current] == TChar.CreateChecked('.')) { if (dotCount >= 3 // Max of 3 dots and 4 segments || !atLeastOneChar // No empty segmets: 1...1 @@ -284,8 +272,10 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int { // end of string, allowed } - else if (name[current] == PrefixSeparators[0] || name[current] == PrefixSeparators[1] - || (notImplicitFile && (name[current] == UrlSeparators[0] || name[current] == UrlSeparators[1] || name[current] == UrlSeparators[2]))) + // For a normal IPv4 address, the terminator is the prefix ('/' or its counterpart, '\'). If notImplicitFile is set, the terminator + // is one of the characters which signify the start of the rest of the URI - the port number (':'), query string ('?') or fragment ('#') + else if (name[current] == TChar.CreateChecked('/') || name[current] == TChar.CreateChecked('\\') + || (notImplicitFile && (name[current] == TChar.CreateChecked(':') || name[current] == TChar.CreateChecked('?') || name[current] == TChar.CreateChecked('#')))) { bytesConsumed = current; } diff --git a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs index 99468b2dbb3fa8..2d00b41ba6b78a 100644 --- a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs @@ -97,17 +97,6 @@ internal static bool ShouldHaveIpv4Embedded(ReadOnlySpan numbers) internal static bool IsValidStrict(ReadOnlySpan name) where TChar : unmanaged, IBinaryInteger { - TChar AddressStartCharacter = TChar.CreateChecked('['); - TChar AddressEndCharacter = TChar.CreateChecked(']'); - TChar ComponentSeparator = TChar.CreateChecked(':'); - TChar ScopeSeparator = TChar.CreateChecked('%'); - TChar PrefixSeparator = TChar.CreateChecked('/'); - TChar PortSeparator = TChar.CreateChecked(':'); - ReadOnlySpan HexadecimalPrefix = [TChar.CreateChecked('0'), TChar.CreateChecked('x')]; - ReadOnlySpan Compressor = [ComponentSeparator, ComponentSeparator]; - - TChar IPv4ComponentSeparator = TChar.CreateChecked('.'); - // Number of components in this IPv6 address int sequenceCount = 0; // Length of the component currently being constructed @@ -121,7 +110,9 @@ internal static bool IsValidStrict(ReadOnlySpan name) bool needsClosingBracket = false; int start = 0; - if (start < name.Length && name[start] == AddressStartCharacter) + // An IPv6 address may begin with a start character ('['). If it does, it must end with an end + // character (']'). + if (start < name.Length && name[start] == TChar.CreateChecked('[')) { start++; needsClosingBracket = true; @@ -133,7 +124,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) } // Starting with a colon character is only valid if another colon follows. - if (name[start] == Compressor[0] && (start + 1 >= name.Length || name[start + 1] != Compressor[1])) + if (name[start] == TChar.CreateChecked(':') && (start + 1 >= name.Length || name[start + 1] != TChar.CreateChecked(':'))) { return false; } @@ -159,7 +150,10 @@ internal static bool IsValidStrict(ReadOnlySpan name) sequenceLength = 0; } - if (name[i] == ScopeSeparator) + // An IPv6 address is separated from its scope by a '%' character. The scope + // is terminated by the natural end of the address, the address end character (']') + // or the start of the prefix ('/'). + if (name[i] == TChar.CreateChecked('%')) { bool moveToNextCharacter = true; @@ -167,8 +161,8 @@ internal static bool IsValidStrict(ReadOnlySpan name) { i++; - if (name[i] == AddressEndCharacter - || name[i] == PrefixSeparator) + if (name[i] == TChar.CreateChecked(']') + || name[i] == TChar.CreateChecked('/')) { moveToNextCharacter = false; break; @@ -181,7 +175,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) } } - if (name[i] == AddressEndCharacter) + if (name[i] == TChar.CreateChecked(']')) { if (!needsClosingBracket) { @@ -191,7 +185,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) // If there's more after the closing bracket, it must be a port. // We don't use the port, but we still validate it. - if (i + 1 < name.Length && name[i + 1] != PortSeparator) + if (i + 1 < name.Length && name[i + 1] != TChar.CreateChecked(':')) { return false; } @@ -201,9 +195,10 @@ internal static bool IsValidStrict(ReadOnlySpan name) // Skip past the closing bracket and the port separator. i += 2; // If there is a port, it must either be a hexadecimal or decimal number. - if (i + 1 < name.Length && name.Slice(i).StartsWith(HexadecimalPrefix)) + // If the next two characters are '0x' or '0X' then it's a hexadecimal number. Skip the prefix. + if (i + 1 < name.Length && name[i] == TChar.CreateChecked('0') && (name[i + 1] | TChar.CreateChecked(0x20)) == TChar.CreateChecked('x')) { - i += HexadecimalPrefix.Length; + i += 2; numericBase = IPAddressParser.Hex; } @@ -217,13 +212,16 @@ internal static bool IsValidStrict(ReadOnlySpan name) } continue; } - else if (name[i] == PrefixSeparator) + // A prefix in an IPv6 address is invalid. + else if (name[i] == TChar.CreateChecked('/')) { return false; } - else if (name[i] == ComponentSeparator) + // IPv6 address components are separated by at least one colon. + else if (name[i] == TChar.CreateChecked(':')) { - if (i > 0 && name.Slice(i - 1, 2).SequenceEqual(Compressor)) + // If the next character after a colon is another colon, the address contains a compressor ('::'). + if (i > 0 && name[i - 1] == TChar.CreateChecked(':')) { if (haveCompressor) { @@ -241,7 +239,8 @@ internal static bool IsValidStrict(ReadOnlySpan name) sequenceLength = 0; continue; } - else if (name[i] == IPv4ComponentSeparator) + // Encountering a '.' indicates that an IPv6 address may contain an embedded IPv4 address. + else if (name[i] == TChar.CreateChecked('.')) { if (haveIPv4Address) { @@ -299,12 +298,12 @@ internal static bool IsValidStrict(ReadOnlySpan name) // numbers // Array filled in with the numbers in the IPv6 groups // - // PrefixLength - // Set to the number after the prefix separator (/) if found + // scopeId + // Set to the text after the scope separator (%) if found // // Assumes: // has been validated and contains only hex digits in groups of - // 16-bit numbers, the characters ':' and '/', and a possible IPv4 + // 16-bit numbers, the characters ':', '/' and '%', and a possible IPv4 // address // // Throws: @@ -314,25 +313,17 @@ internal static bool IsValidStrict(ReadOnlySpan name) internal static void Parse(ReadOnlySpan address, scoped Span numbers, out ReadOnlySpan scopeId) where TChar : unmanaged, IBinaryInteger { - TChar AddressStartCharacter = TChar.CreateChecked('['); - TChar AddressEndCharacter = TChar.CreateChecked(']'); - TChar ComponentSeparator = TChar.CreateChecked(':'); - TChar ScopeSeparator = TChar.CreateChecked('%'); - TChar PrefixSeparator = TChar.CreateChecked('/'); - ReadOnlySpan Compressor = [ComponentSeparator, ComponentSeparator]; - - TChar IPv4ComponentSeparator = TChar.CreateChecked('.'); - int number = 0; int index = 0; int compressorIndex = -1; bool numberIsValid = true; scopeId = ReadOnlySpan.Empty; - for (int i = (address[0] == AddressStartCharacter ? 1 : 0); i < address.Length && address[i] != AddressEndCharacter;) + // Skip the start '[' character, if present. Stop parsing at the end IPv6 address terminator (']'). + for (int i = (address[0] == TChar.CreateChecked('[') ? 1 : 0); i < address.Length && address[i] != TChar.CreateChecked(']');) { - if (address[i] == ScopeSeparator - || address[i] == PrefixSeparator) + if (address[i] == TChar.CreateChecked('%') + || address[i] == TChar.CreateChecked('/')) { if (numberIsValid) { @@ -340,26 +331,29 @@ internal static void Parse(ReadOnlySpan address, scoped Span(ReadOnlySpan address, scoped Span(ReadOnlySpan address, scoped Span(ReadOnlySpan ipSpan, bool tryParse) where TChar : unmanaged, IBinaryInteger { - TChar IPv6ComponentSeparator = TChar.CreateChecked(':'); - Debug.Assert(typeof(TChar) == typeof(byte) || typeof(TChar) == typeof(char)); - if (ipSpan.Contains(IPv6ComponentSeparator)) + if (ipSpan.Contains(TChar.CreateChecked(':'))) { // The address is parsed as IPv6 if and only if it contains a colon. This is valid because // we don't support/parse a port specification at the end of an IPv4 address. From afdee99b1c8caccb4a318f184cd1094a389f1df7 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sun, 23 Jun 2024 19:22:03 +0100 Subject: [PATCH 18/43] Swapped CreateChecked to CreateTruncating --- .../System/Net/IPv4AddressHelper.Common.cs | 28 +++++----- .../System/Net/IPv6AddressHelper.Common.cs | 54 +++++++++---------- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs index 63db77f5d205df..0bb6d9db3d152c 100644 --- a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs @@ -26,9 +26,9 @@ internal static int ParseHostNumber(ReadOnlySpan str) int b = 0; TChar ch; - for (; (start < str.Length) && (ch = str[start]) != TChar.CreateChecked('.') && ch != TChar.CreateChecked(':'); ++start) + for (; (start < str.Length) && (ch = str[start]) != TChar.CreateTruncating('.') && ch != TChar.CreateTruncating(':'); ++start) { - b = (b * 10) + int.CreateTruncating(ch - TChar.CreateChecked('0')); + b = (b * 10) + int.CreateTruncating(ch - TChar.CreateTruncating('0')); } numbers[i] = (byte)b; @@ -120,15 +120,15 @@ internal static bool IsValidCanonical(ReadOnlySpan name, out int b if (allowIPv6) { // For an IPv4 address nested inside an IPv6, the terminator is either ScopeId ('%'), prefix ('/') or ipv6 address terminator (']') - if (ch == TChar.CreateChecked('%') || ch == TChar.CreateChecked('/') || ch == TChar.CreateChecked(']')) + if (ch == TChar.CreateTruncating('%') || ch == TChar.CreateTruncating('/') || ch == TChar.CreateTruncating(']')) { break; } } // For a normal IPv4 address, the terminator is the prefix ('/' or its counterpart, '\'). If notImplicitFile is set, the terminator // is one of the characters which signify the start of the rest of the URI - the port number (':'), query string ('?') or fragment ('#') - else if (ch == TChar.CreateChecked('/') || ch == TChar.CreateChecked('\\') - || (notImplicitFile && (ch == TChar.CreateChecked(':') || ch == TChar.CreateChecked('?') || ch == TChar.CreateChecked('#')))) + else if (ch == TChar.CreateTruncating('/') || ch == TChar.CreateTruncating('\\') + || (notImplicitFile && (ch == TChar.CreateTruncating(':') || ch == TChar.CreateTruncating('?') || ch == TChar.CreateTruncating('#')))) { break; } @@ -136,9 +136,9 @@ internal static bool IsValidCanonical(ReadOnlySpan name, out int b if (IPAddressParser.TryParseInteger(IPAddressParser.Decimal, ch, out int parsedCharacter)) { // A number starting with zero should be interpreted in base 8 / octal - if (!haveNumber && ch == TChar.CreateChecked('0')) + if (!haveNumber && ch == TChar.CreateTruncating('0')) { - if (current + 1 < name.Length && name[current + 1] == TChar.CreateChecked('0')) + if (current + 1 < name.Length && name[current + 1] == TChar.CreateTruncating('0')) { // 00 is not allowed as a prefix. return false; @@ -155,7 +155,7 @@ internal static bool IsValidCanonical(ReadOnlySpan name, out int b } } // If the current character is not an integer, it may be the IPv4 component separator ('.') - else if (ch == TChar.CreateChecked('.')) + else if (ch == TChar.CreateTruncating('.')) { if (!haveNumber || (number > 0 && firstCharIsZero)) { @@ -206,7 +206,7 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int numberBase = IPAddressParser.Decimal; // A number starting with zero should be interpreted in base 8 / octal // If the number starts with 0x, it should be interpreted in base 16 / hex - if (ch == TChar.CreateChecked('0')) + if (ch == TChar.CreateTruncating('0')) { numberBase = IPAddressParser.Octal; current++; @@ -214,9 +214,9 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int if (current < name.Length) { // Force an uppercase 'X' to lowercase 'x'. - ch = name[current] | TChar.CreateChecked(0x20); + ch = name[current] | TChar.CreateTruncating(0x20); - if (ch == TChar.CreateChecked('x')) + if (ch == TChar.CreateTruncating('x')) { numberBase = IPAddressParser.Hex; current++; @@ -245,7 +245,7 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int atLeastOneChar = true; } - if (current < name.Length && name[current] == TChar.CreateChecked('.')) + if (current < name.Length && name[current] == TChar.CreateTruncating('.')) { if (dotCount >= 3 // Max of 3 dots and 4 segments || !atLeastOneChar // No empty segmets: 1...1 @@ -274,8 +274,8 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int } // For a normal IPv4 address, the terminator is the prefix ('/' or its counterpart, '\'). If notImplicitFile is set, the terminator // is one of the characters which signify the start of the rest of the URI - the port number (':'), query string ('?') or fragment ('#') - else if (name[current] == TChar.CreateChecked('/') || name[current] == TChar.CreateChecked('\\') - || (notImplicitFile && (name[current] == TChar.CreateChecked(':') || name[current] == TChar.CreateChecked('?') || name[current] == TChar.CreateChecked('#')))) + else if (name[current] == TChar.CreateTruncating('/') || name[current] == TChar.CreateTruncating('\\') + || (notImplicitFile && (name[current] == TChar.CreateTruncating(':') || name[current] == TChar.CreateTruncating('?') || name[current] == TChar.CreateTruncating('#')))) { bytesConsumed = current; } diff --git a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs index 2d00b41ba6b78a..e2265d44a33ba9 100644 --- a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs @@ -112,7 +112,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) // An IPv6 address may begin with a start character ('['). If it does, it must end with an end // character (']'). - if (start < name.Length && name[start] == TChar.CreateChecked('[')) + if (start < name.Length && name[start] == TChar.CreateTruncating('[')) { start++; needsClosingBracket = true; @@ -124,7 +124,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) } // Starting with a colon character is only valid if another colon follows. - if (name[start] == TChar.CreateChecked(':') && (start + 1 >= name.Length || name[start + 1] != TChar.CreateChecked(':'))) + if (name[start] == TChar.CreateTruncating(':') && (start + 1 >= name.Length || name[start + 1] != TChar.CreateTruncating(':'))) { return false; } @@ -153,7 +153,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) // An IPv6 address is separated from its scope by a '%' character. The scope // is terminated by the natural end of the address, the address end character (']') // or the start of the prefix ('/'). - if (name[i] == TChar.CreateChecked('%')) + if (name[i] == TChar.CreateTruncating('%')) { bool moveToNextCharacter = true; @@ -161,8 +161,8 @@ internal static bool IsValidStrict(ReadOnlySpan name) { i++; - if (name[i] == TChar.CreateChecked(']') - || name[i] == TChar.CreateChecked('/')) + if (name[i] == TChar.CreateTruncating(']') + || name[i] == TChar.CreateTruncating('/')) { moveToNextCharacter = false; break; @@ -175,7 +175,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) } } - if (name[i] == TChar.CreateChecked(']')) + if (name[i] == TChar.CreateTruncating(']')) { if (!needsClosingBracket) { @@ -185,7 +185,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) // If there's more after the closing bracket, it must be a port. // We don't use the port, but we still validate it. - if (i + 1 < name.Length && name[i + 1] != TChar.CreateChecked(':')) + if (i + 1 < name.Length && name[i + 1] != TChar.CreateTruncating(':')) { return false; } @@ -196,7 +196,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) i += 2; // If there is a port, it must either be a hexadecimal or decimal number. // If the next two characters are '0x' or '0X' then it's a hexadecimal number. Skip the prefix. - if (i + 1 < name.Length && name[i] == TChar.CreateChecked('0') && (name[i + 1] | TChar.CreateChecked(0x20)) == TChar.CreateChecked('x')) + if (i + 1 < name.Length && name[i] == TChar.CreateTruncating('0') && (name[i + 1] | TChar.CreateTruncating(0x20)) == TChar.CreateTruncating('x')) { i += 2; @@ -213,15 +213,15 @@ internal static bool IsValidStrict(ReadOnlySpan name) continue; } // A prefix in an IPv6 address is invalid. - else if (name[i] == TChar.CreateChecked('/')) + else if (name[i] == TChar.CreateTruncating('/')) { return false; } // IPv6 address components are separated by at least one colon. - else if (name[i] == TChar.CreateChecked(':')) + else if (name[i] == TChar.CreateTruncating(':')) { // If the next character after a colon is another colon, the address contains a compressor ('::'). - if (i > 0 && name[i - 1] == TChar.CreateChecked(':')) + if (i > 0 && name[i - 1] == TChar.CreateTruncating(':')) { if (haveCompressor) { @@ -240,7 +240,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) continue; } // Encountering a '.' indicates that an IPv6 address may contain an embedded IPv4 address. - else if (name[i] == TChar.CreateChecked('.')) + else if (name[i] == TChar.CreateTruncating('.')) { if (haveIPv4Address) { @@ -320,10 +320,10 @@ internal static void Parse(ReadOnlySpan address, scoped Span.Empty; // Skip the start '[' character, if present. Stop parsing at the end IPv6 address terminator (']'). - for (int i = (address[0] == TChar.CreateChecked('[') ? 1 : 0); i < address.Length && address[i] != TChar.CreateChecked(']');) + for (int i = (address[0] == TChar.CreateTruncating('[') ? 1 : 0); i < address.Length && address[i] != TChar.CreateTruncating(']');) { - if (address[i] == TChar.CreateChecked('%') - || address[i] == TChar.CreateChecked('/')) + if (address[i] == TChar.CreateTruncating('%') + || address[i] == TChar.CreateTruncating('/')) { if (numberIsValid) { @@ -332,28 +332,28 @@ internal static void Parse(ReadOnlySpan address, scoped Span(ReadOnlySpan address, scoped Span(ReadOnlySpan address, scoped Span(ReadOnlySpan address, scoped Span Date: Sun, 23 Jun 2024 20:17:27 +0100 Subject: [PATCH 19/43] Code review feedback: initial work Renamed bytesConsumed parameter to charsConsumed. `IPNetwork.TryParse(ReadOnlySpan, IPNetwork)` now finds the last index of a '/' rather than the first. This syncs its behaviour with the `ReadOnlySpan` equivalent. Removed unnecessary type constraints which lingered after the removal of the class-level generic type constraint. --- .../System/Net/IPv4AddressHelper.Common.cs | 30 +++++++++---------- .../System/Net/IPv6AddressHelper.Common.cs | 2 +- .../src/System/Net/IPNetwork.cs | 2 +- .../src/System/IPv4AddressHelper.cs | 9 +++--- .../src/System/IPv6AddressHelper.cs | 3 +- 5 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs index 0bb6d9db3d152c..2c1d46e61498ff 100644 --- a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs @@ -28,7 +28,7 @@ internal static int ParseHostNumber(ReadOnlySpan str) for (; (start < str.Length) && (ch = str[start]) != TChar.CreateTruncating('.') && ch != TChar.CreateTruncating(':'); ++start) { - b = (b * 10) + int.CreateTruncating(ch - TChar.CreateTruncating('0')); + b = (b * 10) + int.CreateTruncating(ch) - '0'; } numbers[i] = (byte)b; @@ -58,7 +58,7 @@ internal static int ParseHostNumber(ReadOnlySpan str) // the check is made on an unknown scheme (suppress IPv4 canonicalization) // // Outputs: - // bytesConsumed + // charsConsumed // index of last character in we checked // // Assumes: @@ -74,17 +74,17 @@ internal static int ParseHostNumber(ReadOnlySpan str) // //Remark: MUST NOT be used unless all input indexes are verified and trusted. - internal static bool IsValid(ReadOnlySpan name, out int bytesConsumed, bool allowIPv6, bool notImplicitFile, bool unknownScheme) + internal static bool IsValid(ReadOnlySpan name, out int charsConsumed, bool allowIPv6, bool notImplicitFile, bool unknownScheme) where TChar : unmanaged, IBinaryInteger { // IPv6 can only have canonical IPv4 embedded. Unknown schemes will not attempt parsing of non-canonical IPv4 addresses. if (allowIPv6 || unknownScheme) { - return IsValidCanonical(name, out bytesConsumed, allowIPv6, notImplicitFile); + return IsValidCanonical(name, out charsConsumed, allowIPv6, notImplicitFile); } else { - return ParseNonCanonical(name, out bytesConsumed, notImplicitFile) != Invalid; + return ParseNonCanonical(name, out charsConsumed, notImplicitFile) != Invalid; } } @@ -101,7 +101,7 @@ internal static bool IsValid(ReadOnlySpan name, out int bytesConsu // / "2" %x30-34 DIGIT ; 200-249 // / "25" %x30-35 ; 250-255 // - internal static bool IsValidCanonical(ReadOnlySpan name, out int bytesConsumed, bool allowIPv6, bool notImplicitFile) + internal static bool IsValidCanonical(ReadOnlySpan name, out int charsConsumed, bool allowIPv6, bool notImplicitFile) where TChar : unmanaged, IBinaryInteger { int dots = 0; @@ -112,7 +112,7 @@ internal static bool IsValidCanonical(ReadOnlySpan name, out int b Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); - bytesConsumed = 0; + charsConsumed = 0; for (current = 0; current < name.Length; current++) { TChar ch = name[current]; @@ -175,7 +175,7 @@ internal static bool IsValidCanonical(ReadOnlySpan name, out int b bool res = (dots == 3) && haveNumber; if (res) { - bytesConsumed = current; + charsConsumed = current; } return res; } @@ -184,7 +184,7 @@ internal static bool IsValidCanonical(ReadOnlySpan name, out int b // Return Invalid (-1) for failures. // If the address has less than three dots, only the rightmost section is assumed to contain the combined value for // the missing sections: 0xFF00FFFF == 0xFF.0x00.0xFF.0xFF == 0xFF.0xFFFF - internal static long ParseNonCanonical(ReadOnlySpan name, out int bytesConsumed, bool notImplicitFile) + internal static long ParseNonCanonical(ReadOnlySpan name, out int charsConsumed, bool notImplicitFile) where TChar : unmanaged, IBinaryInteger { int numberBase = IPAddressParser.Decimal; @@ -196,7 +196,7 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int int dotCount = 0; // Limit 3 int current; - bytesConsumed = 0; + charsConsumed = 0; for (current = 0; current < name.Length; current++) { TChar ch = name[current]; @@ -277,7 +277,7 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int else if (name[current] == TChar.CreateTruncating('/') || name[current] == TChar.CreateTruncating('\\') || (notImplicitFile && (name[current] == TChar.CreateTruncating(':') || name[current] == TChar.CreateTruncating('?') || name[current] == TChar.CreateTruncating('#')))) { - bytesConsumed = current; + charsConsumed = current; } else { @@ -295,28 +295,28 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int { return Invalid; } - bytesConsumed = current; + charsConsumed = current; return parts[0]; case 1: // 0xFF.0xFFFFFF if (parts[1] > 0xffffff) { return Invalid; } - bytesConsumed = current; + charsConsumed = current; return (parts[0] << 24) | (parts[1] & 0xffffff); case 2: // 0xFF.0xFF.0xFFFF if (parts[2] > 0xffff) { return Invalid; } - bytesConsumed = current; + charsConsumed = current; return (parts[0] << 24) | ((parts[1] & 0xff) << 16) | (parts[2] & 0xffff); case 3: // 0xFF.0xFF.0xFF.0xFF if (parts[3] > 0xff) { return Invalid; } - bytesConsumed = current; + charsConsumed = current; return (parts[0] << 24) | ((parts[1] & 0xff) << 16) | ((parts[2] & 0xff) << 8) | (parts[3] & 0xff); default: return Invalid; diff --git a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs index e2265d44a33ba9..700ca9c2da8244 100644 --- a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs @@ -403,7 +403,7 @@ internal static void Parse(ReadOnlySpan address, scoped Span s, out IPNetwork result) /// if the conversion was successful; otherwise, . public static bool TryParse(ReadOnlySpan utf8Text, [MaybeNullWhen(false)] out IPNetwork result) { - int separatorIndex = utf8Text.IndexOf(byte.CreateTruncating('/')); + int separatorIndex = utf8Text.LastIndexOf((byte)'/'); if (separatorIndex >= 0) { ReadOnlySpan ipAddressSpan = utf8Text.Slice(0, separatorIndex); diff --git a/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs b/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs index f4f0f04110ead8..c9e90d833b1947 100644 --- a/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs +++ b/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers.Binary; using System.Diagnostics; using System.Numerics; @@ -12,8 +13,7 @@ internal static partial class IPv4AddressHelper { // methods // Parse and canonicalize - internal static string ParseCanonicalName(ReadOnlySpan str, ref bool isLoopback) - where TChar : unmanaged, IBinaryInteger + internal static string ParseCanonicalName(ReadOnlySpan str, ref bool isLoopback) { Span numbers = stackalloc byte[NumberOfLabels]; isLoopback = Parse(str, numbers); @@ -36,15 +36,14 @@ internal static string ParseCanonicalName(ReadOnlySpan str, ref bo // // Convert this IPv4 address into a sequence of 4 8-bit numbers // - private static bool Parse(ReadOnlySpan name, Span numbers) - where TChar : unmanaged, IBinaryInteger + private static bool Parse(ReadOnlySpan name, Span numbers) { // "name" parameter includes ports, so bytesConsumed may be different from span length long result = ParseNonCanonical(name, out _, true); Debug.Assert(result != Invalid, $"Failed to parse after already validated: {string.Join(string.Empty, name.ToArray())}"); - System.Buffers.Binary.BinaryPrimitives.WriteUInt32BigEndian(numbers, (uint)result); + BinaryPrimitives.WriteUInt32BigEndian(numbers, (uint)result); return numbers[0] == 127; } diff --git a/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs b/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs index 3c2864d63fd039..1a89173871edd5 100644 --- a/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs +++ b/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs @@ -10,8 +10,7 @@ namespace System.Net // The idea is to stay with static helper methods and strings internal static partial class IPv6AddressHelper { - internal static string ParseCanonicalName(ReadOnlySpan str, ref bool isLoopback, out ReadOnlySpan scopeId) - where TChar : unmanaged, IBinaryInteger + internal static string ParseCanonicalName(ReadOnlySpan str, ref bool isLoopback, out ReadOnlySpan scopeId) { Span numbers = stackalloc ushort[NumberOfLabels]; numbers.Clear(); From c5936944c8200e42c0fb6f170cc855f1c461266d Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sun, 23 Jun 2024 21:05:41 +0100 Subject: [PATCH 20/43] Removed unnecessary lastSequence modification This is only used when handling an IPv4 address embedded inside an IPv6 address, but this can only appear once, so the modification is unnecessary --- src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs b/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs index 1a89173871edd5..617814f6431958 100644 --- a/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs +++ b/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs @@ -251,7 +251,6 @@ private static unsafe bool InternalIsValid(char* name, int start, ref int end, b i = lastSequence + ipv4AddressLength; // ipv4 address takes 2 slots in ipv6 address, one was just counted meeting the '.' ++sequenceCount; - lastSequence = i - sequenceLength; haveIPv4Address = true; --i; // it will be incremented back on the next loop break; From 4aff60e2b037a6182509afd802ca25af0d6e8324 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Tue, 2 Jul 2024 19:32:51 +0100 Subject: [PATCH 21/43] Code review changes * Completed rename process of bytesConsumed to charsConsumed. * Replaced remaining reference of TChar.CreateChecked with CreateTruncating. * Removed unnecessary nullability annotation. * Formatting change --- .../System.Net.Primitives/ref/System.Net.Primitives.cs | 2 +- .../System.Net.Primitives/src/System/Net/IPAddress.cs | 4 ++-- .../src/System/Net/IPAddressParser.cs | 10 +++------- .../System.Net.Primitives/src/System/Net/IPNetwork.cs | 2 +- .../System.Private.Uri/src/System/IPv4AddressHelper.cs | 4 ++-- src/libraries/System.Private.Uri/src/System/Uri.cs | 4 ++-- 6 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs b/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs index 545cf638ee7b7e..ba30816f95887c 100644 --- a/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs +++ b/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs @@ -328,7 +328,7 @@ public IPEndPoint(System.Net.IPAddress address, int port) { } public bool TryFormat(System.Span utf8Destination, out int bytesWritten) { throw null; } public static bool TryParse(System.ReadOnlySpan s, out System.Net.IPNetwork result) { throw null; } public static bool TryParse(string? s, out System.Net.IPNetwork result) { throw null; } - public static bool TryParse(ReadOnlySpan utf8Text, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out IPNetwork result) { throw null; } + public static bool TryParse(ReadOnlySpan utf8Text, out IPNetwork result) { throw null; } } public partial interface IWebProxy { diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs index 8a6288c2f409ca..788639ce8cdd66 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs @@ -251,8 +251,8 @@ public static bool TryParse(ReadOnlySpan ipSpan, [NotNullWhen(true)] out I } /// - static bool IUtf8SpanParsable.TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, [NotNullWhen(true)] out IPAddress? result) - => TryParse(utf8Text, out result); + static bool IUtf8SpanParsable.TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, [NotNullWhen(true)] out IPAddress? result) => + TryParse(utf8Text, out result); /// static bool IParsable.TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [NotNullWhen(true)] out IPAddress? result) => diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs index 82bd73de28bafd..c46f8f869a7499 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs @@ -18,7 +18,7 @@ internal static partial class IPAddressParser { Debug.Assert(typeof(TChar) == typeof(byte) || typeof(TChar) == typeof(char)); - if (ipSpan.Contains(TChar.CreateChecked(':'))) + if (ipSpan.Contains(TChar.CreateTruncating(':'))) { // The address is parsed as IPv6 if and only if it contains a colon. This is valid because // we don't support/parse a port specification at the end of an IPv4 address. @@ -45,9 +45,7 @@ internal static partial class IPAddressParser private static bool TryParseIpv4(ReadOnlySpan ipSpan, out long address) where TChar : unmanaged, IBinaryInteger { - long tmpAddr; - - tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipSpan, out int end, notImplicitFile: true); + long tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipSpan, out int end, notImplicitFile: true); if (tmpAddr != IPv4AddressHelper.Invalid && end == ipSpan.Length) { @@ -68,10 +66,8 @@ private static bool TryParseIPv6(ReadOnlySpan ipSpan, Span Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); Debug.Assert(numbersLength >= IPAddressParserStatics.IPv6AddressShorts); - bool isValid = IPv6AddressHelper.IsValidStrict(ipSpan); - scope = 0; - if (isValid) + if (IPv6AddressHelper.IsValidStrict(ipSpan)) { IPv6AddressHelper.Parse(ipSpan, numbers, out ReadOnlySpan scopeIdSpan); diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPNetwork.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPNetwork.cs index e0f6271ffbe32c..b788451efe85d1 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPNetwork.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPNetwork.cs @@ -222,7 +222,7 @@ public static bool TryParse(ReadOnlySpan s, out IPNetwork result) /// A UTF-8 character span that defines an IP network in CIDR notation. /// When the method returns, contains an instance if the conversion succeeds. /// if the conversion was successful; otherwise, . - public static bool TryParse(ReadOnlySpan utf8Text, [MaybeNullWhen(false)] out IPNetwork result) + public static bool TryParse(ReadOnlySpan utf8Text, out IPNetwork result) { int separatorIndex = utf8Text.LastIndexOf((byte)'/'); if (separatorIndex >= 0) diff --git a/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs b/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs index c9e90d833b1947..0cbcd07af69c0d 100644 --- a/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs +++ b/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs @@ -38,10 +38,10 @@ internal static string ParseCanonicalName(ReadOnlySpan str, ref bool isLoo // private static bool Parse(ReadOnlySpan name, Span numbers) { - // "name" parameter includes ports, so bytesConsumed may be different from span length + // "name" parameter includes ports, so charsConsumed may be different from span length long result = ParseNonCanonical(name, out _, true); - Debug.Assert(result != Invalid, $"Failed to parse after already validated: {string.Join(string.Empty, name.ToArray())}"); + Debug.Assert(result != Invalid, $"Failed to parse after already validated: {name}"); BinaryPrimitives.WriteUInt32BigEndian(numbers, (uint)result); diff --git a/src/libraries/System.Private.Uri/src/System/Uri.cs b/src/libraries/System.Private.Uri/src/System/Uri.cs index 32e7c8a0620f76..791296e1c6237a 100644 --- a/src/libraries/System.Private.Uri/src/System/Uri.cs +++ b/src/libraries/System.Private.Uri/src/System/Uri.cs @@ -3861,9 +3861,9 @@ private unsafe int CheckAuthorityHelper(char* pString, int idx, int length, } } else if (char.IsAsciiDigit(ch) && syntax.InFact(UriSyntaxFlags.AllowIPv4Host) && - IPv4AddressHelper.IsValid(new ReadOnlySpan(pString + start, end - start), out int bytesConsumed, false, StaticNotAny(flags, Flags.ImplicitFile), syntax.InFact(UriSyntaxFlags.V1_UnknownUri))) + IPv4AddressHelper.IsValid(new ReadOnlySpan(pString + start, end - start), out int charsConsumed, false, StaticNotAny(flags, Flags.ImplicitFile), syntax.InFact(UriSyntaxFlags.V1_UnknownUri))) { - end = start + bytesConsumed; + end = start + charsConsumed; flags |= Flags.IPv4HostType; if (hasUnicode) From 8c41d9b8b583292bdeb00dbc94b7bacb6603eee5 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Thu, 4 Jul 2024 17:01:33 +0100 Subject: [PATCH 22/43] Optimisations to IPv[4/6]AddressHelper * Removed use of helper method for number parsing. * Removed redundant check of parts[0] in IPv4AddressHelper. * Added basic heuristic to IPv6AddressHelper to avoid searching for an embedded IPv4 address if the string doesn't contain a '.'. --- .../System/Net/IPv4AddressHelper.Common.cs | 52 +++++++++---- .../System/Net/IPv6AddressHelper.Common.cs | 75 +++++++++++++------ 2 files changed, 88 insertions(+), 39 deletions(-) diff --git a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs index 2c1d46e61498ff..1f3e286bde5946 100644 --- a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs @@ -24,11 +24,11 @@ internal static int ParseHostNumber(ReadOnlySpan str) for (int i = 0; i < numbers.Length; ++i) { int b = 0; - TChar ch; + int ch; - for (; (start < str.Length) && (ch = str[start]) != TChar.CreateTruncating('.') && ch != TChar.CreateTruncating(':'); ++start) + for (; (start < str.Length) && (ch = int.CreateTruncating(str[start])) != '.' && ch != ':'; ++start) { - b = (b * 10) + int.CreateTruncating(ch) - '0'; + b = (b * 10) + ch - '0'; } numbers[i] = (byte)b; @@ -133,10 +133,12 @@ internal static bool IsValidCanonical(ReadOnlySpan name, out int c break; } - if (IPAddressParser.TryParseInteger(IPAddressParser.Decimal, ch, out int parsedCharacter)) + int parsedCharacter = int.CreateTruncating(ch) - '0'; + + if (parsedCharacter >= 0 && parsedCharacter <= 9) { // A number starting with zero should be interpreted in base 8 / octal - if (!haveNumber && ch == TChar.CreateTruncating('0')) + if (!haveNumber && parsedCharacter == 0) { if (current + 1 < name.Length && name[current + 1] == TChar.CreateTruncating('0')) { @@ -196,10 +198,13 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int int dotCount = 0; // Limit 3 int current; + Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); + charsConsumed = 0; for (current = 0; current < name.Length; current++) { TChar ch = name[current]; + int maxDigitValue = 9; currentValue = 0; // Figure out what base this section is in, default to base 10 @@ -209,16 +214,19 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int if (ch == TChar.CreateTruncating('0')) { numberBase = IPAddressParser.Octal; + maxDigitValue = 7; + current++; atLeastOneChar = true; if (current < name.Length) { - // Force an uppercase 'X' to lowercase 'x'. - ch = name[current] | TChar.CreateTruncating(0x20); + ch = name[current]; - if (ch == TChar.CreateTruncating('x')) + if ((ch == TChar.CreateTruncating('x')) || (ch == TChar.CreateTruncating('X'))) { numberBase = IPAddressParser.Hex; + maxDigitValue = 9; + current++; atLeastOneChar = false; } @@ -229,10 +237,26 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int for (; current < name.Length; current++) { ch = name[current]; + int characterValue = int.CreateTruncating(ch); + int digitValue = characterValue - '0'; - if (!IPAddressParser.TryParseInteger(numberBase, ch, out int digitValue)) + if ((digitValue < 0) || (digitValue > maxDigitValue)) { - break; // Invalid/terminator + if (numberBase != IPAddressParser.Hex) + { + break; // Invalid/terminator + } + else + { + int lowercaseCharacterValue = characterValue | 0x20; + + digitValue = 10 + lowercaseCharacterValue - 'a'; + + if ((digitValue < 10) || (digitValue > 16)) + { + break; // Invalid/terminator + } + } } currentValue = (currentValue * numberBase) + digitValue; @@ -248,7 +272,7 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int if (current < name.Length && name[current] == TChar.CreateTruncating('.')) { if (dotCount >= 3 // Max of 3 dots and 4 segments - || !atLeastOneChar // No empty segmets: 1...1 + || !atLeastOneChar // No empty segments: 1...1 // Only the last segment can be more than 255 (if there are less than 3 dots) || currentValue > 0xFF) { @@ -259,7 +283,7 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int atLeastOneChar = false; continue; } - // We don't get here unless We find an invalid character or a terminator + // We don't get here unless we find an invalid character or a terminator break; } @@ -291,10 +315,6 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int switch (dotCount) { case 0: // 0xFFFFFFFF - if (parts[0] > MaxIPv4Value) - { - return Invalid; - } charsConsumed = current; return parts[0]; case 1: // 0xFF.0xFFFFFF diff --git a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs index 700ca9c2da8244..ea3455900db3b3 100644 --- a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs @@ -132,7 +132,10 @@ internal static bool IsValidStrict(ReadOnlySpan name) int i; for (i = start; i < name.Length; ++i) { - if (IPAddressParser.IsValidInteger(IPAddressParser.Hex, name[i])) + int hexCh = int.CreateTruncating(name[i]) | 0x20; + + if ((hexCh >= '0' && hexCh <= '9') + || (hexCh >= 'a' && hexCh <= 'f')) { ++sequenceLength; expectingNumber = false; @@ -205,7 +208,16 @@ internal static bool IsValidStrict(ReadOnlySpan name) for (; i < name.Length; i++) { - if (!IPAddressParser.IsValidInteger(numericBase, name[i])) + int ch = int.CreateTruncating(name[i]) | 0x20; + + if ((ch >= '0' && ch <= '9') + || (numericBase == IPAddressParser.Hex + && ch >= 'a' + && ch <= 'f')) + { + continue; + } + else { return false; } @@ -317,10 +329,13 @@ internal static void Parse(ReadOnlySpan address, scoped Span.Empty; // Skip the start '[' character, if present. Stop parsing at the end IPv6 address terminator (']'). - for (int i = (address[0] == TChar.CreateTruncating('[') ? 1 : 0); i < address.Length && address[i] != TChar.CreateTruncating(']');) + for (int i = (address[0] == TChar.CreateTruncating('[') ? 1 : 0); i < end;) { if (address[i] == TChar.CreateTruncating('%') || address[i] == TChar.CreateTruncating('/')) @@ -353,37 +368,43 @@ internal static void Parse(ReadOnlySpan address, scoped Span(ReadOnlySpan address, scoped Span 9)) + { + int lowercaseCharacterValue = characterValue | 0x20; + + digitValue = 10 + lowercaseCharacterValue - 'a'; + } + + number = number * IPAddressParser.Hex + digitValue; } } @@ -416,20 +448,17 @@ internal static void Parse(ReadOnlySpan address, scoped Span 0) + // If index is the same as NumberOfLabels, it means that "zero bits" are already in the correct place. + // It happens for leading and trailing compression. + if (compressorIndex > 0 && index != NumberOfLabels) { int toIndex = NumberOfLabels - 1; int fromIndex = index - 1; - // if fromIndex and toIndex are the same, it means that "zero bits" are already in the correct place - // it happens for leading and trailing compression - if (fromIndex != toIndex) + for (int i = index - compressorIndex; i > 0; --i) { - for (int i = index - compressorIndex; i > 0; --i) - { - numbers[toIndex--] = numbers[fromIndex]; - numbers[fromIndex--] = 0; - } + numbers[toIndex--] = numbers[fromIndex]; + numbers[fromIndex--] = 0; } } } From 64c639cdd693eb68d4ee35a2c97ee6959c59ec43 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Thu, 4 Jul 2024 18:22:48 +0100 Subject: [PATCH 23/43] Cleaned up IPAddressParser.Common.cs IPAddressParser.Common.cs is no longer needed. This should also clean up the PR diff --- .../src/System/Net/IPAddressParser.Common.cs | 46 ------------------- .../System/Net/IPv4AddressHelper.Common.cs | 16 ++++--- .../System/Net/IPv6AddressHelper.Common.cs | 10 ++-- .../src/System.Net.Primitives.csproj | 2 - .../src/System/Net/IPAddressParser.cs | 5 +- .../System.Net.Primitives.Pal.Tests.csproj | 2 - ...stem.Net.Primitives.UnitTests.Tests.csproj | 2 - .../src/System.Net.Quic.csproj | 1 - .../System.Net.Security.sln | 36 +++++++++------ .../src/System.Net.Security.csproj | 2 - .../System.Net.Security.Unit.Tests.csproj | 2 - .../src/System.Private.Uri.csproj | 1 - 12 files changed, 42 insertions(+), 83 deletions(-) delete mode 100644 src/libraries/Common/src/System/Net/IPAddressParser.Common.cs diff --git a/src/libraries/Common/src/System/Net/IPAddressParser.Common.cs b/src/libraries/Common/src/System/Net/IPAddressParser.Common.cs deleted file mode 100644 index 11122b325b8a26..00000000000000 --- a/src/libraries/Common/src/System/Net/IPAddressParser.Common.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics; -using System.Numerics; - -namespace System.Net -{ - internal static partial class IPAddressParser - { - public const int Octal = 8; - public const int Decimal = 10; - public const int Hex = 16; - - internal const int MaxIPv4StringLength = 15; // 4 numbers separated by 3 periods, with up to 3 digits per number - internal const int MaxIPv6StringLength = 65; - - public static bool IsValidInteger(int numericBase, TChar ch) - where TChar : unmanaged, IBinaryInteger - => IsValidInteger(numericBase, int.CreateTruncating(ch)); - - private static bool IsValidInteger(int numericBase, int characterValue) - { - Debug.Assert(numericBase is Octal or Decimal or Hex); - - return numericBase <= Decimal - ? characterValue >= '0' && characterValue < '0' + numericBase - : HexConverter.IsHexChar(characterValue); - } - - public static bool TryParseInteger(int numericBase, TChar ch, out int parsedNumber) - where TChar : unmanaged, IBinaryInteger - { - int characterValue = int.CreateTruncating(ch); - bool validNumber = IsValidInteger(numericBase, characterValue); - - // HexConverter allows digits 1-F to be mapped to integers. The octal/decimal digit range restrictions are performed - // in IsValidInteger. - parsedNumber = validNumber - ? HexConverter.FromChar(characterValue) - : -1; - - return validNumber; - } - } -} diff --git a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs index 1f3e286bde5946..7bd509b1e9940b 100644 --- a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs @@ -12,6 +12,10 @@ internal static partial class IPv4AddressHelper internal const long Invalid = -1; private const long MaxIPv4Value = uint.MaxValue; // the native parser cannot handle MaxIPv4Value, only MaxIPv4Value - 1 + private const int Octal = 8; + private const int Decimal = 10; + private const int Hex = 16; + private const int NumberOfLabels = 4; // Only called from the IPv6Helper, only parse the canonical format @@ -150,7 +154,7 @@ internal static bool IsValidCanonical(ReadOnlySpan name, out int c } haveNumber = true; - number = number * IPAddressParser.Decimal + parsedCharacter; + number = number * Decimal + parsedCharacter; if (number > byte.MaxValue) { return false; @@ -189,7 +193,7 @@ internal static bool IsValidCanonical(ReadOnlySpan name, out int c internal static long ParseNonCanonical(ReadOnlySpan name, out int charsConsumed, bool notImplicitFile) where TChar : unmanaged, IBinaryInteger { - int numberBase = IPAddressParser.Decimal; + int numberBase = IPv4AddressHelper.Decimal; Span parts = stackalloc long[4]; long currentValue = 0; bool atLeastOneChar = false; @@ -208,12 +212,12 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int currentValue = 0; // Figure out what base this section is in, default to base 10 - numberBase = IPAddressParser.Decimal; + numberBase = IPv4AddressHelper.Decimal; // A number starting with zero should be interpreted in base 8 / octal // If the number starts with 0x, it should be interpreted in base 16 / hex if (ch == TChar.CreateTruncating('0')) { - numberBase = IPAddressParser.Octal; + numberBase = IPv4AddressHelper.Octal; maxDigitValue = 7; current++; @@ -224,7 +228,7 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int if ((ch == TChar.CreateTruncating('x')) || (ch == TChar.CreateTruncating('X'))) { - numberBase = IPAddressParser.Hex; + numberBase = IPv4AddressHelper.Hex; maxDigitValue = 9; current++; @@ -242,7 +246,7 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int if ((digitValue < 0) || (digitValue > maxDigitValue)) { - if (numberBase != IPAddressParser.Hex) + if (numberBase != IPv4AddressHelper.Hex) { break; // Invalid/terminator } diff --git a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs index ea3455900db3b3..538f99044c0b03 100644 --- a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs @@ -8,6 +8,8 @@ namespace System.Net { internal static partial class IPv6AddressHelper { + private const int Decimal = 10; + private const int Hex = 16; private const int NumberOfLabels = 8; // RFC 5952 Section 4.2.3 @@ -193,7 +195,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) return false; } - int numericBase = IPAddressParser.Decimal; + int numericBase = IPv6AddressHelper.Decimal; // Skip past the closing bracket and the port separator. i += 2; @@ -203,7 +205,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) { i += 2; - numericBase = IPAddressParser.Hex; + numericBase = IPv6AddressHelper.Hex; } for (; i < name.Length; i++) @@ -211,7 +213,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) int ch = int.CreateTruncating(name[i]) | 0x20; if ((ch >= '0' && ch <= '9') - || (numericBase == IPAddressParser.Hex + || (numericBase == IPv6AddressHelper.Hex && ch >= 'a' && ch <= 'f')) { @@ -435,7 +437,7 @@ internal static void Parse(ReadOnlySpan address, scoped Span - (ReadOnlySpan ipSpan, bool tryParse) where TChar : unmanaged, IBinaryInteger { diff --git a/src/libraries/System.Net.Primitives/tests/PalTests/System.Net.Primitives.Pal.Tests.csproj b/src/libraries/System.Net.Primitives/tests/PalTests/System.Net.Primitives.Pal.Tests.csproj index 4d2f7a7b210dc9..90fbc1e3d31bab 100644 --- a/src/libraries/System.Net.Primitives/tests/PalTests/System.Net.Primitives.Pal.Tests.csproj +++ b/src/libraries/System.Net.Primitives/tests/PalTests/System.Net.Primitives.Pal.Tests.csproj @@ -20,8 +20,6 @@ Link="ProductionCode\System\Net\Sockets\SocketError.cs" /> - - - diff --git a/src/libraries/System.Net.Security/System.Net.Security.sln b/src/libraries/System.Net.Security/System.Net.Security.sln index 6eb84cc7b74e95..541d6d6dc7fdc9 100644 --- a/src/libraries/System.Net.Security/System.Net.Security.sln +++ b/src/libraries/System.Net.Security/System.Net.Security.sln @@ -1,4 +1,8 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35017.193 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StreamConformanceTests", "..\Common\tests\StreamConformanceTests\StreamConformanceTests.csproj", "{8A5732F4-1562-4E5A-B2FF-E294ABC5EEFD}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{16B2F746-485C-4A15-883E-ABF2C885DD2A}" @@ -65,11 +69,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{70E29C1C-AC9 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{29A8D8F5-27EF-44B7-8C71-FC4E2D10EBBA}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "tools\gen", "{3E6CB661-AA4C-477E-8747-2D2852A5B3C4}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{3E6CB661-AA4C-477E-8747-2D2852A5B3C4}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "tools\src", "{5AEC3A9F-78FF-4358-8B71-88294A4548C9}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5AEC3A9F-78FF-4358-8B71-88294A4548C9}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "tools\ref", "{8D0B384E-98D9-4B04-823D-69B81F8E7920}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{8D0B384E-98D9-4B04-823D-69B81F8E7920}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{9CE6A0C3-971B-41D2-90E7-7ACC1D7AB598}" EndProject @@ -202,38 +206,42 @@ Global GlobalSection(NestedProjects) = preSolution {8A5732F4-1562-4E5A-B2FF-E294ABC5EEFD} = {3F0D8988-4AFB-4AE2-9BA2-07E08765CCD1} {16B2F746-485C-4A15-883E-ABF2C885DD2A} = {3F0D8988-4AFB-4AE2-9BA2-07E08765CCD1} - {3B8B3E64-BF3B-46E6-8203-AA6D7D6A0F6D} = {3F0D8988-4AFB-4AE2-9BA2-07E08765CCD1} - {39FCDE49-DBC3-442B-8B3E-8A5FDCAE8B28} = {3F0D8988-4AFB-4AE2-9BA2-07E08765CCD1} - {E67AFB3A-4DEC-48A3-9B78-02FFAC7FF0EA} = {3F0D8988-4AFB-4AE2-9BA2-07E08765CCD1} {9B96EE70-512D-4254-BE5D-2329C046E2D3} = {19C079E3-367B-480E-A46E-8D56D47F94EA} {80607BD5-FDA3-442D-AB29-FF9D72ECA52D} = {19C079E3-367B-480E-A46E-8D56D47F94EA} {65D6EAAE-CC4D-4099-BE6D-424FB850139D} = {19C079E3-367B-480E-A46E-8D56D47F94EA} {CED10233-29CC-4531-8C4F-C8AB882E4225} = {19C079E3-367B-480E-A46E-8D56D47F94EA} + {A8972DBC-DE9B-4CA0-8EA4-7F15BF24DB88} = {70E29C1C-AC9B-4E71-AB74-9906EF973EBF} {42FAFAFA-7B57-4930-AF14-185AC04C68BD} = {19C079E3-367B-480E-A46E-8D56D47F94EA} {E272FDA2-EE69-457C-A844-E05DA143DE5B} = {19C079E3-367B-480E-A46E-8D56D47F94EA} {97493212-ADF3-46AF-8669-F038964CDA22} = {19C079E3-367B-480E-A46E-8D56D47F94EA} + {20F20695-DB18-47EB-A005-AA432ADCF865} = {70E29C1C-AC9B-4E71-AB74-9906EF973EBF} + {3B8B3E64-BF3B-46E6-8203-AA6D7D6A0F6D} = {3F0D8988-4AFB-4AE2-9BA2-07E08765CCD1} + {39FCDE49-DBC3-442B-8B3E-8A5FDCAE8B28} = {3F0D8988-4AFB-4AE2-9BA2-07E08765CCD1} + {E67AFB3A-4DEC-48A3-9B78-02FFAC7FF0EA} = {3F0D8988-4AFB-4AE2-9BA2-07E08765CCD1} + {756F6023-9F34-4E0E-8D9D-A561BFB8BFF8} = {29A8D8F5-27EF-44B7-8C71-FC4E2D10EBBA} + {F492B006-8C7C-471C-90C4-08C6A28F250A} = {29A8D8F5-27EF-44B7-8C71-FC4E2D10EBBA} + {BBF8C38F-E4A7-42E3-AEEB-8A2D08E9FF7E} = {29A8D8F5-27EF-44B7-8C71-FC4E2D10EBBA} {43DB92AB-9319-4E3D-955F-1551DDC5D9D0} = {19C079E3-367B-480E-A46E-8D56D47F94EA} {E33D29AF-A224-4E84-AFBF-DD05DB365B42} = {19C079E3-367B-480E-A46E-8D56D47F94EA} {374F2563-9C40-46E9-A80A-95C4B84404C3} = {19C079E3-367B-480E-A46E-8D56D47F94EA} {A536EE74-5844-4474-B8F6-EDACE7D35CEA} = {19C079E3-367B-480E-A46E-8D56D47F94EA} {53C8C6E5-0DCF-4A4B-880A-803A337CA728} = {19C079E3-367B-480E-A46E-8D56D47F94EA} {603A0443-18D0-418B-A823-A47F3C223006} = {19C079E3-367B-480E-A46E-8D56D47F94EA} - {A8972DBC-DE9B-4CA0-8EA4-7F15BF24DB88} = {70E29C1C-AC9B-4E71-AB74-9906EF973EBF} - {20F20695-DB18-47EB-A005-AA432ADCF865} = {70E29C1C-AC9B-4E71-AB74-9906EF973EBF} {80F5CE39-D33D-4CDD-ACD2-FA7597B6CBB1} = {70E29C1C-AC9B-4E71-AB74-9906EF973EBF} - {756F6023-9F34-4E0E-8D9D-A561BFB8BFF8} = {29A8D8F5-27EF-44B7-8C71-FC4E2D10EBBA} - {F492B006-8C7C-471C-90C4-08C6A28F250A} = {29A8D8F5-27EF-44B7-8C71-FC4E2D10EBBA} - {BBF8C38F-E4A7-42E3-AEEB-8A2D08E9FF7E} = {29A8D8F5-27EF-44B7-8C71-FC4E2D10EBBA} {BBC4D81D-C03E-455A-A294-0EC91581AC61} = {3E6CB661-AA4C-477E-8747-2D2852A5B3C4} {40C4360F-4012-4FBA-BA89-C06C8893AEDC} = {3E6CB661-AA4C-477E-8747-2D2852A5B3C4} - {3E6CB661-AA4C-477E-8747-2D2852A5B3C4} = {9CE6A0C3-971B-41D2-90E7-7ACC1D7AB598} {754229AE-72DF-4B65-A72E-8AC7B23737DC} = {5AEC3A9F-78FF-4358-8B71-88294A4548C9} {3A8F9FAF-F801-4BCA-A3B8-0834526657DF} = {5AEC3A9F-78FF-4358-8B71-88294A4548C9} - {5AEC3A9F-78FF-4358-8B71-88294A4548C9} = {9CE6A0C3-971B-41D2-90E7-7ACC1D7AB598} {1AADC546-C5B7-4247-9557-FFF414A8C639} = {8D0B384E-98D9-4B04-823D-69B81F8E7920} + {3E6CB661-AA4C-477E-8747-2D2852A5B3C4} = {9CE6A0C3-971B-41D2-90E7-7ACC1D7AB598} + {5AEC3A9F-78FF-4358-8B71-88294A4548C9} = {9CE6A0C3-971B-41D2-90E7-7ACC1D7AB598} {8D0B384E-98D9-4B04-823D-69B81F8E7920} = {9CE6A0C3-971B-41D2-90E7-7ACC1D7AB598} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4803416B-CE32-4730-905F-645A9B766D2B} EndGlobalSection + GlobalSection(SharedMSBuildProjectFiles) = preSolution + ..\..\tools\illink\src\ILLink.Shared\ILLink.Shared.projitems*{3a8f9faf-f801-4bca-a3b8-0834526657df}*SharedItemsImports = 5 + ..\..\tools\illink\src\ILLink.Shared\ILLink.Shared.projitems*{40c4360f-4012-4fba-ba89-c06c8893aedc}*SharedItemsImports = 5 + EndGlobalSection EndGlobal diff --git a/src/libraries/System.Net.Security/src/System.Net.Security.csproj b/src/libraries/System.Net.Security/src/System.Net.Security.csproj index 18ca63bdb3595c..2e263173ad81de 100644 --- a/src/libraries/System.Net.Security/src/System.Net.Security.csproj +++ b/src/libraries/System.Net.Security/src/System.Net.Security.csproj @@ -71,8 +71,6 @@ - - - From f290a3a09ec4ee681d0d4e0c39c051f0b67c2c51 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Thu, 4 Jul 2024 18:25:31 +0100 Subject: [PATCH 24/43] .sln file cleanup --- .../System.Net.Security.sln | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/src/libraries/System.Net.Security/System.Net.Security.sln b/src/libraries/System.Net.Security/System.Net.Security.sln index 541d6d6dc7fdc9..6eb84cc7b74e95 100644 --- a/src/libraries/System.Net.Security/System.Net.Security.sln +++ b/src/libraries/System.Net.Security/System.Net.Security.sln @@ -1,8 +1,4 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.11.35017.193 -MinimumVisualStudioVersion = 10.0.40219.1 +Microsoft Visual Studio Solution File, Format Version 12.00 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StreamConformanceTests", "..\Common\tests\StreamConformanceTests\StreamConformanceTests.csproj", "{8A5732F4-1562-4E5A-B2FF-E294ABC5EEFD}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{16B2F746-485C-4A15-883E-ABF2C885DD2A}" @@ -69,11 +65,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{70E29C1C-AC9 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{29A8D8F5-27EF-44B7-8C71-FC4E2D10EBBA}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{3E6CB661-AA4C-477E-8747-2D2852A5B3C4}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "tools\gen", "{3E6CB661-AA4C-477E-8747-2D2852A5B3C4}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5AEC3A9F-78FF-4358-8B71-88294A4548C9}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "tools\src", "{5AEC3A9F-78FF-4358-8B71-88294A4548C9}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{8D0B384E-98D9-4B04-823D-69B81F8E7920}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "tools\ref", "{8D0B384E-98D9-4B04-823D-69B81F8E7920}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{9CE6A0C3-971B-41D2-90E7-7ACC1D7AB598}" EndProject @@ -206,42 +202,38 @@ Global GlobalSection(NestedProjects) = preSolution {8A5732F4-1562-4E5A-B2FF-E294ABC5EEFD} = {3F0D8988-4AFB-4AE2-9BA2-07E08765CCD1} {16B2F746-485C-4A15-883E-ABF2C885DD2A} = {3F0D8988-4AFB-4AE2-9BA2-07E08765CCD1} + {3B8B3E64-BF3B-46E6-8203-AA6D7D6A0F6D} = {3F0D8988-4AFB-4AE2-9BA2-07E08765CCD1} + {39FCDE49-DBC3-442B-8B3E-8A5FDCAE8B28} = {3F0D8988-4AFB-4AE2-9BA2-07E08765CCD1} + {E67AFB3A-4DEC-48A3-9B78-02FFAC7FF0EA} = {3F0D8988-4AFB-4AE2-9BA2-07E08765CCD1} {9B96EE70-512D-4254-BE5D-2329C046E2D3} = {19C079E3-367B-480E-A46E-8D56D47F94EA} {80607BD5-FDA3-442D-AB29-FF9D72ECA52D} = {19C079E3-367B-480E-A46E-8D56D47F94EA} {65D6EAAE-CC4D-4099-BE6D-424FB850139D} = {19C079E3-367B-480E-A46E-8D56D47F94EA} {CED10233-29CC-4531-8C4F-C8AB882E4225} = {19C079E3-367B-480E-A46E-8D56D47F94EA} - {A8972DBC-DE9B-4CA0-8EA4-7F15BF24DB88} = {70E29C1C-AC9B-4E71-AB74-9906EF973EBF} {42FAFAFA-7B57-4930-AF14-185AC04C68BD} = {19C079E3-367B-480E-A46E-8D56D47F94EA} {E272FDA2-EE69-457C-A844-E05DA143DE5B} = {19C079E3-367B-480E-A46E-8D56D47F94EA} {97493212-ADF3-46AF-8669-F038964CDA22} = {19C079E3-367B-480E-A46E-8D56D47F94EA} - {20F20695-DB18-47EB-A005-AA432ADCF865} = {70E29C1C-AC9B-4E71-AB74-9906EF973EBF} - {3B8B3E64-BF3B-46E6-8203-AA6D7D6A0F6D} = {3F0D8988-4AFB-4AE2-9BA2-07E08765CCD1} - {39FCDE49-DBC3-442B-8B3E-8A5FDCAE8B28} = {3F0D8988-4AFB-4AE2-9BA2-07E08765CCD1} - {E67AFB3A-4DEC-48A3-9B78-02FFAC7FF0EA} = {3F0D8988-4AFB-4AE2-9BA2-07E08765CCD1} - {756F6023-9F34-4E0E-8D9D-A561BFB8BFF8} = {29A8D8F5-27EF-44B7-8C71-FC4E2D10EBBA} - {F492B006-8C7C-471C-90C4-08C6A28F250A} = {29A8D8F5-27EF-44B7-8C71-FC4E2D10EBBA} - {BBF8C38F-E4A7-42E3-AEEB-8A2D08E9FF7E} = {29A8D8F5-27EF-44B7-8C71-FC4E2D10EBBA} {43DB92AB-9319-4E3D-955F-1551DDC5D9D0} = {19C079E3-367B-480E-A46E-8D56D47F94EA} {E33D29AF-A224-4E84-AFBF-DD05DB365B42} = {19C079E3-367B-480E-A46E-8D56D47F94EA} {374F2563-9C40-46E9-A80A-95C4B84404C3} = {19C079E3-367B-480E-A46E-8D56D47F94EA} {A536EE74-5844-4474-B8F6-EDACE7D35CEA} = {19C079E3-367B-480E-A46E-8D56D47F94EA} {53C8C6E5-0DCF-4A4B-880A-803A337CA728} = {19C079E3-367B-480E-A46E-8D56D47F94EA} {603A0443-18D0-418B-A823-A47F3C223006} = {19C079E3-367B-480E-A46E-8D56D47F94EA} + {A8972DBC-DE9B-4CA0-8EA4-7F15BF24DB88} = {70E29C1C-AC9B-4E71-AB74-9906EF973EBF} + {20F20695-DB18-47EB-A005-AA432ADCF865} = {70E29C1C-AC9B-4E71-AB74-9906EF973EBF} {80F5CE39-D33D-4CDD-ACD2-FA7597B6CBB1} = {70E29C1C-AC9B-4E71-AB74-9906EF973EBF} + {756F6023-9F34-4E0E-8D9D-A561BFB8BFF8} = {29A8D8F5-27EF-44B7-8C71-FC4E2D10EBBA} + {F492B006-8C7C-471C-90C4-08C6A28F250A} = {29A8D8F5-27EF-44B7-8C71-FC4E2D10EBBA} + {BBF8C38F-E4A7-42E3-AEEB-8A2D08E9FF7E} = {29A8D8F5-27EF-44B7-8C71-FC4E2D10EBBA} {BBC4D81D-C03E-455A-A294-0EC91581AC61} = {3E6CB661-AA4C-477E-8747-2D2852A5B3C4} {40C4360F-4012-4FBA-BA89-C06C8893AEDC} = {3E6CB661-AA4C-477E-8747-2D2852A5B3C4} + {3E6CB661-AA4C-477E-8747-2D2852A5B3C4} = {9CE6A0C3-971B-41D2-90E7-7ACC1D7AB598} {754229AE-72DF-4B65-A72E-8AC7B23737DC} = {5AEC3A9F-78FF-4358-8B71-88294A4548C9} {3A8F9FAF-F801-4BCA-A3B8-0834526657DF} = {5AEC3A9F-78FF-4358-8B71-88294A4548C9} - {1AADC546-C5B7-4247-9557-FFF414A8C639} = {8D0B384E-98D9-4B04-823D-69B81F8E7920} - {3E6CB661-AA4C-477E-8747-2D2852A5B3C4} = {9CE6A0C3-971B-41D2-90E7-7ACC1D7AB598} {5AEC3A9F-78FF-4358-8B71-88294A4548C9} = {9CE6A0C3-971B-41D2-90E7-7ACC1D7AB598} + {1AADC546-C5B7-4247-9557-FFF414A8C639} = {8D0B384E-98D9-4B04-823D-69B81F8E7920} {8D0B384E-98D9-4B04-823D-69B81F8E7920} = {9CE6A0C3-971B-41D2-90E7-7ACC1D7AB598} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4803416B-CE32-4730-905F-645A9B766D2B} EndGlobalSection - GlobalSection(SharedMSBuildProjectFiles) = preSolution - ..\..\tools\illink\src\ILLink.Shared\ILLink.Shared.projitems*{3a8f9faf-f801-4bca-a3b8-0834526657df}*SharedItemsImports = 5 - ..\..\tools\illink\src\ILLink.Shared\ILLink.Shared.projitems*{40c4360f-4012-4fba-ba89-c06c8893aedc}*SharedItemsImports = 5 - EndGlobalSection EndGlobal From 65027dbe06e78746261cd722603d4eac41ec5906 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 6 Jul 2024 13:13:14 +0100 Subject: [PATCH 25/43] Testing micro-optimisations in IPv4 address parser * Slightly reducing memory usage by storing the parts as uints. * Removing unnecessary assignment of the maximum digit value for octal, deferring another. * Removed case-insensitivity in the hexadecimal parsing. * Remove unnecessary bitmasks in final assembly of address - values have already been verified as < 0xFF. --- .../System/Net/IPv4AddressHelper.Common.cs | 63 ++++++++++--------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs index 7bd509b1e9940b..b2f0477ec17d40 100644 --- a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs @@ -194,7 +194,7 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int where TChar : unmanaged, IBinaryInteger { int numberBase = IPv4AddressHelper.Decimal; - Span parts = stackalloc long[4]; + Span parts = stackalloc uint[4]; long currentValue = 0; bool atLeastOneChar = false; @@ -208,7 +208,7 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int for (current = 0; current < name.Length; current++) { TChar ch = name[current]; - int maxDigitValue = 9; + int maxCharacterValue = '9'; currentValue = 0; // Figure out what base this section is in, default to base 10 @@ -217,9 +217,6 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int // If the number starts with 0x, it should be interpreted in base 16 / hex if (ch == TChar.CreateTruncating('0')) { - numberBase = IPv4AddressHelper.Octal; - maxDigitValue = 7; - current++; atLeastOneChar = true; if (current < name.Length) @@ -229,11 +226,15 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int if ((ch == TChar.CreateTruncating('x')) || (ch == TChar.CreateTruncating('X'))) { numberBase = IPv4AddressHelper.Hex; - maxDigitValue = 9; current++; atLeastOneChar = false; } + else + { + numberBase = IPv4AddressHelper.Octal; + maxCharacterValue = '7'; + } } } @@ -242,26 +243,31 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int { ch = name[current]; int characterValue = int.CreateTruncating(ch); - int digitValue = characterValue - '0'; + int digitValue; - if ((digitValue < 0) || (digitValue > maxDigitValue)) + if (characterValue >= '0' && characterValue <= maxCharacterValue) { - if (numberBase != IPv4AddressHelper.Hex) + digitValue = characterValue - '0'; + } + else if (numberBase == IPv4AddressHelper.Hex) + { + if (characterValue >= 'a' && characterValue <= 'f') { - break; // Invalid/terminator + digitValue = 10 + characterValue - 'a'; + } + else if (characterValue >= 'A' && characterValue <= 'F') + { + digitValue = 10 + characterValue - 'A'; } else { - int lowercaseCharacterValue = characterValue | 0x20; - - digitValue = 10 + lowercaseCharacterValue - 'a'; - - if ((digitValue < 10) || (digitValue > 16)) - { - break; // Invalid/terminator - } + break; // Invalid/terminator } } + else + { + break; // Invalid/terminator + } currentValue = (currentValue * numberBase) + digitValue; @@ -273,7 +279,7 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int atLeastOneChar = true; } - if (current < name.Length && name[current] == TChar.CreateTruncating('.')) + if (ch == TChar.CreateTruncating('.')) { if (dotCount >= 3 // Max of 3 dots and 4 segments || !atLeastOneChar // No empty segments: 1...1 @@ -282,7 +288,7 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int { return Invalid; } - parts[dotCount] = currentValue; + parts[dotCount] = unchecked((uint)currentValue); dotCount++; atLeastOneChar = false; continue; @@ -305,7 +311,7 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int else if (name[current] == TChar.CreateTruncating('/') || name[current] == TChar.CreateTruncating('\\') || (notImplicitFile && (name[current] == TChar.CreateTruncating(':') || name[current] == TChar.CreateTruncating('?') || name[current] == TChar.CreateTruncating('#')))) { - charsConsumed = current; + // end of string, (as terminated) allowed } else { @@ -313,35 +319,32 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int return Invalid; } - parts[dotCount] = currentValue; + parts[dotCount] = unchecked((uint)currentValue); + charsConsumed = current; - // Parsed, reassemble and check for overflows + // Parsed, reassemble and check for overflows in the last part. Previous parts have already been checked in the loop switch (dotCount) { case 0: // 0xFFFFFFFF - charsConsumed = current; return parts[0]; case 1: // 0xFF.0xFFFFFF if (parts[1] > 0xffffff) { return Invalid; } - charsConsumed = current; - return (parts[0] << 24) | (parts[1] & 0xffffff); + return (parts[0] << 24) | parts[1]; case 2: // 0xFF.0xFF.0xFFFF if (parts[2] > 0xffff) { return Invalid; } - charsConsumed = current; - return (parts[0] << 24) | ((parts[1] & 0xff) << 16) | (parts[2] & 0xffff); + return (parts[0] << 24) | (parts[1] << 16) | parts[2]; case 3: // 0xFF.0xFF.0xFF.0xFF if (parts[3] > 0xff) { return Invalid; } - charsConsumed = current; - return (parts[0] << 24) | ((parts[1] & 0xff) << 16) | ((parts[2] & 0xff) << 8) | (parts[3] & 0xff); + return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]; default: return Invalid; } From cd73915810aea3c4b9d6394b9287725bc635fecd Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 6 Jul 2024 17:08:25 +0100 Subject: [PATCH 26/43] Added "in" qualifier to Span/ROS parameters --- .../System/Net/IPv4AddressHelper.Common.cs | 12 +++---- .../System/Net/IPv6AddressHelper.Common.cs | 17 ++++++---- .../Net/Security/TargetHostNameHelper.cs | 4 +-- .../src/System/Net/IPAddressParser.cs | 31 +++++++++++-------- .../UnitTests/Fakes/IPv4AddressHelper.cs | 2 +- .../UnitTests/Fakes/IPv6AddressHelper.cs | 8 ++--- .../src/System/IPv4AddressHelper.cs | 6 ++-- .../src/System/IPv6AddressHelper.cs | 4 +-- 8 files changed, 47 insertions(+), 37 deletions(-) diff --git a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs index b2f0477ec17d40..04727f5a2dae06 100644 --- a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs @@ -19,7 +19,7 @@ internal static partial class IPv4AddressHelper private const int NumberOfLabels = 4; // Only called from the IPv6Helper, only parse the canonical format - internal static int ParseHostNumber(ReadOnlySpan str) + internal static int ParseHostNumber(in ReadOnlySpan str) where TChar : unmanaged, IBinaryInteger { Span numbers = stackalloc byte[NumberOfLabels]; @@ -78,17 +78,17 @@ internal static int ParseHostNumber(ReadOnlySpan str) // //Remark: MUST NOT be used unless all input indexes are verified and trusted. - internal static bool IsValid(ReadOnlySpan name, out int charsConsumed, bool allowIPv6, bool notImplicitFile, bool unknownScheme) + internal static bool IsValid(in ReadOnlySpan name, out int charsConsumed, bool allowIPv6, bool notImplicitFile, bool unknownScheme) where TChar : unmanaged, IBinaryInteger { // IPv6 can only have canonical IPv4 embedded. Unknown schemes will not attempt parsing of non-canonical IPv4 addresses. if (allowIPv6 || unknownScheme) { - return IsValidCanonical(name, out charsConsumed, allowIPv6, notImplicitFile); + return IsValidCanonical(in name, out charsConsumed, allowIPv6, notImplicitFile); } else { - return ParseNonCanonical(name, out charsConsumed, notImplicitFile) != Invalid; + return ParseNonCanonical(in name, out charsConsumed, notImplicitFile) != Invalid; } } @@ -105,7 +105,7 @@ internal static bool IsValid(ReadOnlySpan name, out int charsConsu // / "2" %x30-34 DIGIT ; 200-249 // / "25" %x30-35 ; 250-255 // - internal static bool IsValidCanonical(ReadOnlySpan name, out int charsConsumed, bool allowIPv6, bool notImplicitFile) + internal static bool IsValidCanonical(in ReadOnlySpan name, out int charsConsumed, bool allowIPv6, bool notImplicitFile) where TChar : unmanaged, IBinaryInteger { int dots = 0; @@ -190,7 +190,7 @@ internal static bool IsValidCanonical(ReadOnlySpan name, out int c // Return Invalid (-1) for failures. // If the address has less than three dots, only the rightmost section is assumed to contain the combined value for // the missing sections: 0xFF00FFFF == 0xFF.0x00.0xFF.0xFF == 0xFF.0xFFFF - internal static long ParseNonCanonical(ReadOnlySpan name, out int charsConsumed, bool notImplicitFile) + internal static long ParseNonCanonical(in ReadOnlySpan name, out int charsConsumed, bool notImplicitFile) where TChar : unmanaged, IBinaryInteger { int numberBase = IPv4AddressHelper.Decimal; diff --git a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs index 538f99044c0b03..22a529a44a5894 100644 --- a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs @@ -15,7 +15,7 @@ internal static partial class IPv6AddressHelper // RFC 5952 Section 4.2.3 // Longest consecutive sequence of zero segments, minimum 2. // On equal, first sequence wins. <-1, -1> for no compression. - internal static (int longestSequenceStart, int longestSequenceLength) FindCompressionRange(ReadOnlySpan numbers) + internal static (int longestSequenceStart, int longestSequenceLength) FindCompressionRange(in ReadOnlySpan numbers) { int longestSequenceLength = 0, longestSequenceStart = -1, currentSequenceLength = 0; @@ -43,7 +43,7 @@ internal static (int longestSequenceStart, int longestSequenceLength) FindCompre // Returns true if the IPv6 address should be formatted with an embedded IPv4 address: // ::192.168.1.1 - internal static bool ShouldHaveIpv4Embedded(ReadOnlySpan numbers) + internal static bool ShouldHaveIpv4Embedded(in ReadOnlySpan numbers) { // 0:0 : 0:0 : x:x : x.x.x.x if (numbers[0] == 0 && numbers[1] == 0 && numbers[2] == 0 && numbers[3] == 0 && numbers[6] != 0) @@ -96,7 +96,7 @@ internal static bool ShouldHaveIpv4Embedded(ReadOnlySpan numbers) // Remarks: MUST NOT be used unless all input indexes are verified and trusted. // start must be next to '[' position, or error is reported - internal static bool IsValidStrict(ReadOnlySpan name) + internal static bool IsValidStrict(in ReadOnlySpan name) where TChar : unmanaged, IBinaryInteger { // Number of components in this IPv6 address @@ -261,7 +261,9 @@ internal static bool IsValidStrict(ReadOnlySpan name) return false; } - if (!IPv4AddressHelper.IsValid(name.Slice(lastSequence), out int ipv4AddressLength, true, false, false)) + ReadOnlySpan remainderOfSequence = name.Slice(lastSequence); + + if (!IPv4AddressHelper.IsValid(in remainderOfSequence, out int ipv4AddressLength, true, false, false)) { return false; } @@ -324,7 +326,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) // Nothing // - internal static void Parse(ReadOnlySpan address, scoped Span numbers, out ReadOnlySpan scopeId) + internal static void Parse(in ReadOnlySpan address, scoped Span numbers, out ReadOnlySpan scopeId) where TChar : unmanaged, IBinaryInteger { int number = 0; @@ -411,7 +413,10 @@ internal static void Parse(ReadOnlySpan address, scoped Span ipv4Address = address.Slice(i, j - i); + + number = IPv4AddressHelper.ParseHostNumber(in ipv4Address); numbers[index++] = (ushort)(number >> 16); numbers[index++] = (ushort)number; i = j; diff --git a/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs b/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs index 0ac5e9e8542eec..660bebff2c90b4 100644 --- a/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs +++ b/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs @@ -55,11 +55,11 @@ internal static bool IsValidAddress(string? hostname) // we don't support/parse a port specification at the end of an IPv4 address. Span numbers = stackalloc ushort[IPAddressParserStatics.IPv6AddressShorts]; - return IPv6AddressHelper.IsValidStrict(ipSpan); + return IPv6AddressHelper.IsValidStrict(in ipSpan); } else if (char.IsDigit(ipSpan[0])) { - long tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipSpan, out int end, notImplicitFile: true); + long tmpAddr = IPv4AddressHelper.ParseNonCanonical(in ipSpan, out int end, notImplicitFile: true); if (tmpAddr != IPv4AddressHelper.Invalid && end == ipSpan.Length) { diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs index 22808d92a474fe..b28f171bf5de22 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs @@ -16,7 +16,7 @@ internal static class IPAddressParser internal const int MaxIPv4StringLength = 15; // 4 numbers separated by 3 periods, with up to 3 digits per number internal const int MaxIPv6StringLength = 65; - internal static IPAddress? Parse(ReadOnlySpan ipSpan, bool tryParse) + internal static IPAddress? Parse(scoped in ReadOnlySpan ipSpan, bool tryParse) where TChar : unmanaged, IBinaryInteger { Debug.Assert(typeof(TChar) == typeof(byte) || typeof(TChar) == typeof(char)); @@ -27,12 +27,12 @@ internal static class IPAddressParser // we don't support/parse a port specification at the end of an IPv4 address. Span numbers = stackalloc ushort[IPAddressParserStatics.IPv6AddressShorts]; numbers.Clear(); - if (TryParseIPv6(ipSpan, numbers, IPAddressParserStatics.IPv6AddressShorts, out uint scope)) + if (TryParseIPv6(in ipSpan, numbers, IPAddressParserStatics.IPv6AddressShorts, out uint scope)) { return new IPAddress(numbers, scope); } } - else if (TryParseIpv4(ipSpan, out long address)) + else if (TryParseIpv4(in ipSpan, out long address)) { return new IPAddress(address); } @@ -45,10 +45,10 @@ internal static class IPAddressParser throw new FormatException(SR.dns_bad_ip_address, new SocketException(SocketError.InvalidArgument)); } - private static bool TryParseIpv4(ReadOnlySpan ipSpan, out long address) + private static bool TryParseIpv4(in ReadOnlySpan ipSpan, out long address) where TChar : unmanaged, IBinaryInteger { - long tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipSpan, out int end, notImplicitFile: true); + long tmpAddr = IPv4AddressHelper.ParseNonCanonical(in ipSpan, out int end, notImplicitFile: true); if (tmpAddr != IPv4AddressHelper.Invalid && end == ipSpan.Length) { @@ -63,16 +63,16 @@ private static bool TryParseIpv4(ReadOnlySpan ipSpan, out long add return false; } - private static bool TryParseIPv6(ReadOnlySpan ipSpan, Span numbers, int numbersLength, out uint scope) + private static bool TryParseIPv6(in ReadOnlySpan ipSpan, in Span numbers, int numbersLength, out uint scope) where TChar : unmanaged, IBinaryInteger { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); Debug.Assert(numbersLength >= IPAddressParserStatics.IPv6AddressShorts); scope = 0; - if (IPv6AddressHelper.IsValidStrict(ipSpan)) + if (IPv6AddressHelper.IsValidStrict(in ipSpan)) { - IPv6AddressHelper.Parse(ipSpan, numbers, out ReadOnlySpan scopeIdSpan); + IPv6AddressHelper.Parse(in ipSpan, in numbers, out ReadOnlySpan scopeIdSpan); if (scopeIdSpan.Length > 1) { @@ -171,11 +171,15 @@ internal static int FormatIPv6Address(ushort[] address, uint scopeId, Spa { int pos = 0; - if (IPv6AddressHelper.ShouldHaveIpv4Embedded(address)) + ReadOnlySpan addressSpan = address; + ReadOnlySpan formattableAddresses; + + if (IPv6AddressHelper.ShouldHaveIpv4Embedded(in addressSpan)) { // We need to treat the last 2 ushorts as a 4-byte IPv4 address, // so output the first 6 ushorts normally, followed by the IPv4 address. - AppendSections(address.AsSpan(0, 6), destination, ref pos); + formattableAddresses = addressSpan.Slice(0, 6); + AppendSections(in formattableAddresses, destination, ref pos); if (destination[pos - 1] != TChar.CreateTruncating(':')) { destination[pos++] = TChar.CreateTruncating(':'); @@ -187,7 +191,8 @@ internal static int FormatIPv6Address(ushort[] address, uint scopeId, Spa { // No IPv4 address. Output all 8 sections as part of the IPv6 address // with normal formatting rules. - AppendSections(address.AsSpan(0, 8), destination, ref pos); + formattableAddresses = addressSpan.Slice(0, 8); + AppendSections(in formattableAddresses, destination, ref pos); } // If there's a scope ID, append it. @@ -214,10 +219,10 @@ internal static int FormatIPv6Address(ushort[] address, uint scopeId, Spa // Appends each of the numbers in address in indexed range [fromInclusive, toExclusive), // while also replacing the longest sequence of 0s found in that range with "::", as long // as the sequence is more than one 0. - static void AppendSections(ReadOnlySpan address, Span destination, ref int offset) + static void AppendSections(in ReadOnlySpan address, Span destination, ref int offset) { // Find the longest sequence of zeros to be combined into a "::" - (int zeroStart, int zeroEnd) = IPv6AddressHelper.FindCompressionRange(address); + (int zeroStart, int zeroEnd) = IPv6AddressHelper.FindCompressionRange(in address); bool needsColon = false; // Handle a zero sequence if there is one diff --git a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs index 0810a29d6589f4..a3cafdf35ba6b6 100644 --- a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs +++ b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs @@ -8,7 +8,7 @@ namespace System.Net internal static class IPv4AddressHelper { internal const int Invalid = -1; - internal static unsafe long ParseNonCanonical(ReadOnlySpan name, out int bytesConsumed, bool notImplicitFile) + internal static unsafe long ParseNonCanonical(in ReadOnlySpan name, out int bytesConsumed, bool notImplicitFile) where TChar : unmanaged, IBinaryInteger { bytesConsumed = 0; diff --git a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs index 37262b6471b30c..be9510a8a5b359 100644 --- a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs +++ b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs @@ -9,12 +9,12 @@ namespace System.Net internal static class IPv6AddressHelper { internal static unsafe (int longestSequenceStart, int longestSequenceLength) FindCompressionRange( - ReadOnlySpan numbers) => (-1, -1); - internal static unsafe bool ShouldHaveIpv4Embedded(ReadOnlySpan numbers) => false; - internal static unsafe bool IsValidStrict(ReadOnlySpan name) + in ReadOnlySpan numbers) => (-1, -1); + internal static unsafe bool ShouldHaveIpv4Embedded(in ReadOnlySpan numbers) => false; + internal static unsafe bool IsValidStrict(in ReadOnlySpan name) where TChar : unmanaged, IBinaryInteger => false; - internal static unsafe bool Parse(ReadOnlySpan address, Span numbers, out ReadOnlySpan scopeId) + internal static unsafe bool Parse(in ReadOnlySpan address, scoped Span numbers, out ReadOnlySpan scopeId) where TChar : unmanaged, IBinaryInteger { scopeId = ReadOnlySpan.Empty; diff --git a/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs b/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs index 0cbcd07af69c0d..9eb3dc5b2b6346 100644 --- a/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs +++ b/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs @@ -13,7 +13,7 @@ internal static partial class IPv4AddressHelper { // methods // Parse and canonicalize - internal static string ParseCanonicalName(ReadOnlySpan str, ref bool isLoopback) + internal static string ParseCanonicalName(in ReadOnlySpan str, ref bool isLoopback) { Span numbers = stackalloc byte[NumberOfLabels]; isLoopback = Parse(str, numbers); @@ -36,10 +36,10 @@ internal static string ParseCanonicalName(ReadOnlySpan str, ref bool isLoo // // Convert this IPv4 address into a sequence of 4 8-bit numbers // - private static bool Parse(ReadOnlySpan name, Span numbers) + private static bool Parse(in ReadOnlySpan name, Span numbers) { // "name" parameter includes ports, so charsConsumed may be different from span length - long result = ParseNonCanonical(name, out _, true); + long result = ParseNonCanonical(in name, out _, true); Debug.Assert(result != Invalid, $"Failed to parse after already validated: {name}"); diff --git a/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs b/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs index 617814f6431958..448d8435ff55ab 100644 --- a/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs +++ b/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs @@ -10,11 +10,11 @@ namespace System.Net // The idea is to stay with static helper methods and strings internal static partial class IPv6AddressHelper { - internal static string ParseCanonicalName(ReadOnlySpan str, ref bool isLoopback, out ReadOnlySpan scopeId) + internal static string ParseCanonicalName(in ReadOnlySpan str, ref bool isLoopback, out ReadOnlySpan scopeId) { Span numbers = stackalloc ushort[NumberOfLabels]; numbers.Clear(); - Parse(str, numbers, out scopeId); + Parse(in str, numbers, out scopeId); isLoopback = IsLoopback(numbers); // RFC 5952 Sections 4 & 5 - Compressed, lower case, with possible embedded IPv4 addresses. From 385ee62b8fe38bcdbc0ed44a9ab83e8680f627f6 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 6 Jul 2024 18:12:55 +0100 Subject: [PATCH 27/43] Correcting trailing "in" reference --- .../System.Net.Primitives/src/System/Net/IPAddressParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs index b28f171bf5de22..92b94d3010bd7e 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs @@ -72,7 +72,7 @@ private static bool TryParseIPv6(in ReadOnlySpan ipSpan, in Span scopeIdSpan); + IPv6AddressHelper.Parse(in ipSpan, numbers, out ReadOnlySpan scopeIdSpan); if (scopeIdSpan.Length > 1) { From f7d4298110afe03727a871f69ba94b9196745de3 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 6 Jul 2024 20:11:23 +0100 Subject: [PATCH 28/43] Reverted addition of "in" modifier --- .../System/Net/IPv4AddressHelper.Common.cs | 12 +++---- .../System/Net/IPv6AddressHelper.Common.cs | 17 ++++------ .../Net/Security/TargetHostNameHelper.cs | 4 +-- .../src/System/Net/IPAddressParser.cs | 31 ++++++++----------- .../UnitTests/Fakes/IPv4AddressHelper.cs | 2 +- .../UnitTests/Fakes/IPv6AddressHelper.cs | 8 ++--- .../src/System/IPv4AddressHelper.cs | 6 ++-- .../src/System/IPv6AddressHelper.cs | 4 +-- 8 files changed, 37 insertions(+), 47 deletions(-) diff --git a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs index 04727f5a2dae06..b2f0477ec17d40 100644 --- a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs @@ -19,7 +19,7 @@ internal static partial class IPv4AddressHelper private const int NumberOfLabels = 4; // Only called from the IPv6Helper, only parse the canonical format - internal static int ParseHostNumber(in ReadOnlySpan str) + internal static int ParseHostNumber(ReadOnlySpan str) where TChar : unmanaged, IBinaryInteger { Span numbers = stackalloc byte[NumberOfLabels]; @@ -78,17 +78,17 @@ internal static int ParseHostNumber(in ReadOnlySpan str) // //Remark: MUST NOT be used unless all input indexes are verified and trusted. - internal static bool IsValid(in ReadOnlySpan name, out int charsConsumed, bool allowIPv6, bool notImplicitFile, bool unknownScheme) + internal static bool IsValid(ReadOnlySpan name, out int charsConsumed, bool allowIPv6, bool notImplicitFile, bool unknownScheme) where TChar : unmanaged, IBinaryInteger { // IPv6 can only have canonical IPv4 embedded. Unknown schemes will not attempt parsing of non-canonical IPv4 addresses. if (allowIPv6 || unknownScheme) { - return IsValidCanonical(in name, out charsConsumed, allowIPv6, notImplicitFile); + return IsValidCanonical(name, out charsConsumed, allowIPv6, notImplicitFile); } else { - return ParseNonCanonical(in name, out charsConsumed, notImplicitFile) != Invalid; + return ParseNonCanonical(name, out charsConsumed, notImplicitFile) != Invalid; } } @@ -105,7 +105,7 @@ internal static bool IsValid(in ReadOnlySpan name, out int charsCo // / "2" %x30-34 DIGIT ; 200-249 // / "25" %x30-35 ; 250-255 // - internal static bool IsValidCanonical(in ReadOnlySpan name, out int charsConsumed, bool allowIPv6, bool notImplicitFile) + internal static bool IsValidCanonical(ReadOnlySpan name, out int charsConsumed, bool allowIPv6, bool notImplicitFile) where TChar : unmanaged, IBinaryInteger { int dots = 0; @@ -190,7 +190,7 @@ internal static bool IsValidCanonical(in ReadOnlySpan name, out in // Return Invalid (-1) for failures. // If the address has less than three dots, only the rightmost section is assumed to contain the combined value for // the missing sections: 0xFF00FFFF == 0xFF.0x00.0xFF.0xFF == 0xFF.0xFFFF - internal static long ParseNonCanonical(in ReadOnlySpan name, out int charsConsumed, bool notImplicitFile) + internal static long ParseNonCanonical(ReadOnlySpan name, out int charsConsumed, bool notImplicitFile) where TChar : unmanaged, IBinaryInteger { int numberBase = IPv4AddressHelper.Decimal; diff --git a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs index 22a529a44a5894..538f99044c0b03 100644 --- a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs @@ -15,7 +15,7 @@ internal static partial class IPv6AddressHelper // RFC 5952 Section 4.2.3 // Longest consecutive sequence of zero segments, minimum 2. // On equal, first sequence wins. <-1, -1> for no compression. - internal static (int longestSequenceStart, int longestSequenceLength) FindCompressionRange(in ReadOnlySpan numbers) + internal static (int longestSequenceStart, int longestSequenceLength) FindCompressionRange(ReadOnlySpan numbers) { int longestSequenceLength = 0, longestSequenceStart = -1, currentSequenceLength = 0; @@ -43,7 +43,7 @@ internal static (int longestSequenceStart, int longestSequenceLength) FindCompre // Returns true if the IPv6 address should be formatted with an embedded IPv4 address: // ::192.168.1.1 - internal static bool ShouldHaveIpv4Embedded(in ReadOnlySpan numbers) + internal static bool ShouldHaveIpv4Embedded(ReadOnlySpan numbers) { // 0:0 : 0:0 : x:x : x.x.x.x if (numbers[0] == 0 && numbers[1] == 0 && numbers[2] == 0 && numbers[3] == 0 && numbers[6] != 0) @@ -96,7 +96,7 @@ internal static bool ShouldHaveIpv4Embedded(in ReadOnlySpan numbers) // Remarks: MUST NOT be used unless all input indexes are verified and trusted. // start must be next to '[' position, or error is reported - internal static bool IsValidStrict(in ReadOnlySpan name) + internal static bool IsValidStrict(ReadOnlySpan name) where TChar : unmanaged, IBinaryInteger { // Number of components in this IPv6 address @@ -261,9 +261,7 @@ internal static bool IsValidStrict(in ReadOnlySpan name) return false; } - ReadOnlySpan remainderOfSequence = name.Slice(lastSequence); - - if (!IPv4AddressHelper.IsValid(in remainderOfSequence, out int ipv4AddressLength, true, false, false)) + if (!IPv4AddressHelper.IsValid(name.Slice(lastSequence), out int ipv4AddressLength, true, false, false)) { return false; } @@ -326,7 +324,7 @@ internal static bool IsValidStrict(in ReadOnlySpan name) // Nothing // - internal static void Parse(in ReadOnlySpan address, scoped Span numbers, out ReadOnlySpan scopeId) + internal static void Parse(ReadOnlySpan address, scoped Span numbers, out ReadOnlySpan scopeId) where TChar : unmanaged, IBinaryInteger { int number = 0; @@ -413,10 +411,7 @@ internal static void Parse(in ReadOnlySpan address, scoped Span ipv4Address = address.Slice(i, j - i); - - number = IPv4AddressHelper.ParseHostNumber(in ipv4Address); + number = IPv4AddressHelper.ParseHostNumber(address.Slice(i, j - i)); numbers[index++] = (ushort)(number >> 16); numbers[index++] = (ushort)number; i = j; diff --git a/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs b/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs index 660bebff2c90b4..0ac5e9e8542eec 100644 --- a/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs +++ b/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs @@ -55,11 +55,11 @@ internal static bool IsValidAddress(string? hostname) // we don't support/parse a port specification at the end of an IPv4 address. Span numbers = stackalloc ushort[IPAddressParserStatics.IPv6AddressShorts]; - return IPv6AddressHelper.IsValidStrict(in ipSpan); + return IPv6AddressHelper.IsValidStrict(ipSpan); } else if (char.IsDigit(ipSpan[0])) { - long tmpAddr = IPv4AddressHelper.ParseNonCanonical(in ipSpan, out int end, notImplicitFile: true); + long tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipSpan, out int end, notImplicitFile: true); if (tmpAddr != IPv4AddressHelper.Invalid && end == ipSpan.Length) { diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs index 92b94d3010bd7e..22808d92a474fe 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs @@ -16,7 +16,7 @@ internal static class IPAddressParser internal const int MaxIPv4StringLength = 15; // 4 numbers separated by 3 periods, with up to 3 digits per number internal const int MaxIPv6StringLength = 65; - internal static IPAddress? Parse(scoped in ReadOnlySpan ipSpan, bool tryParse) + internal static IPAddress? Parse(ReadOnlySpan ipSpan, bool tryParse) where TChar : unmanaged, IBinaryInteger { Debug.Assert(typeof(TChar) == typeof(byte) || typeof(TChar) == typeof(char)); @@ -27,12 +27,12 @@ internal static class IPAddressParser // we don't support/parse a port specification at the end of an IPv4 address. Span numbers = stackalloc ushort[IPAddressParserStatics.IPv6AddressShorts]; numbers.Clear(); - if (TryParseIPv6(in ipSpan, numbers, IPAddressParserStatics.IPv6AddressShorts, out uint scope)) + if (TryParseIPv6(ipSpan, numbers, IPAddressParserStatics.IPv6AddressShorts, out uint scope)) { return new IPAddress(numbers, scope); } } - else if (TryParseIpv4(in ipSpan, out long address)) + else if (TryParseIpv4(ipSpan, out long address)) { return new IPAddress(address); } @@ -45,10 +45,10 @@ internal static class IPAddressParser throw new FormatException(SR.dns_bad_ip_address, new SocketException(SocketError.InvalidArgument)); } - private static bool TryParseIpv4(in ReadOnlySpan ipSpan, out long address) + private static bool TryParseIpv4(ReadOnlySpan ipSpan, out long address) where TChar : unmanaged, IBinaryInteger { - long tmpAddr = IPv4AddressHelper.ParseNonCanonical(in ipSpan, out int end, notImplicitFile: true); + long tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipSpan, out int end, notImplicitFile: true); if (tmpAddr != IPv4AddressHelper.Invalid && end == ipSpan.Length) { @@ -63,16 +63,16 @@ private static bool TryParseIpv4(in ReadOnlySpan ipSpan, out long return false; } - private static bool TryParseIPv6(in ReadOnlySpan ipSpan, in Span numbers, int numbersLength, out uint scope) + private static bool TryParseIPv6(ReadOnlySpan ipSpan, Span numbers, int numbersLength, out uint scope) where TChar : unmanaged, IBinaryInteger { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); Debug.Assert(numbersLength >= IPAddressParserStatics.IPv6AddressShorts); scope = 0; - if (IPv6AddressHelper.IsValidStrict(in ipSpan)) + if (IPv6AddressHelper.IsValidStrict(ipSpan)) { - IPv6AddressHelper.Parse(in ipSpan, numbers, out ReadOnlySpan scopeIdSpan); + IPv6AddressHelper.Parse(ipSpan, numbers, out ReadOnlySpan scopeIdSpan); if (scopeIdSpan.Length > 1) { @@ -171,15 +171,11 @@ internal static int FormatIPv6Address(ushort[] address, uint scopeId, Spa { int pos = 0; - ReadOnlySpan addressSpan = address; - ReadOnlySpan formattableAddresses; - - if (IPv6AddressHelper.ShouldHaveIpv4Embedded(in addressSpan)) + if (IPv6AddressHelper.ShouldHaveIpv4Embedded(address)) { // We need to treat the last 2 ushorts as a 4-byte IPv4 address, // so output the first 6 ushorts normally, followed by the IPv4 address. - formattableAddresses = addressSpan.Slice(0, 6); - AppendSections(in formattableAddresses, destination, ref pos); + AppendSections(address.AsSpan(0, 6), destination, ref pos); if (destination[pos - 1] != TChar.CreateTruncating(':')) { destination[pos++] = TChar.CreateTruncating(':'); @@ -191,8 +187,7 @@ internal static int FormatIPv6Address(ushort[] address, uint scopeId, Spa { // No IPv4 address. Output all 8 sections as part of the IPv6 address // with normal formatting rules. - formattableAddresses = addressSpan.Slice(0, 8); - AppendSections(in formattableAddresses, destination, ref pos); + AppendSections(address.AsSpan(0, 8), destination, ref pos); } // If there's a scope ID, append it. @@ -219,10 +214,10 @@ internal static int FormatIPv6Address(ushort[] address, uint scopeId, Spa // Appends each of the numbers in address in indexed range [fromInclusive, toExclusive), // while also replacing the longest sequence of 0s found in that range with "::", as long // as the sequence is more than one 0. - static void AppendSections(in ReadOnlySpan address, Span destination, ref int offset) + static void AppendSections(ReadOnlySpan address, Span destination, ref int offset) { // Find the longest sequence of zeros to be combined into a "::" - (int zeroStart, int zeroEnd) = IPv6AddressHelper.FindCompressionRange(in address); + (int zeroStart, int zeroEnd) = IPv6AddressHelper.FindCompressionRange(address); bool needsColon = false; // Handle a zero sequence if there is one diff --git a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs index a3cafdf35ba6b6..0810a29d6589f4 100644 --- a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs +++ b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs @@ -8,7 +8,7 @@ namespace System.Net internal static class IPv4AddressHelper { internal const int Invalid = -1; - internal static unsafe long ParseNonCanonical(in ReadOnlySpan name, out int bytesConsumed, bool notImplicitFile) + internal static unsafe long ParseNonCanonical(ReadOnlySpan name, out int bytesConsumed, bool notImplicitFile) where TChar : unmanaged, IBinaryInteger { bytesConsumed = 0; diff --git a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs index be9510a8a5b359..37262b6471b30c 100644 --- a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs +++ b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs @@ -9,12 +9,12 @@ namespace System.Net internal static class IPv6AddressHelper { internal static unsafe (int longestSequenceStart, int longestSequenceLength) FindCompressionRange( - in ReadOnlySpan numbers) => (-1, -1); - internal static unsafe bool ShouldHaveIpv4Embedded(in ReadOnlySpan numbers) => false; - internal static unsafe bool IsValidStrict(in ReadOnlySpan name) + ReadOnlySpan numbers) => (-1, -1); + internal static unsafe bool ShouldHaveIpv4Embedded(ReadOnlySpan numbers) => false; + internal static unsafe bool IsValidStrict(ReadOnlySpan name) where TChar : unmanaged, IBinaryInteger => false; - internal static unsafe bool Parse(in ReadOnlySpan address, scoped Span numbers, out ReadOnlySpan scopeId) + internal static unsafe bool Parse(ReadOnlySpan address, Span numbers, out ReadOnlySpan scopeId) where TChar : unmanaged, IBinaryInteger { scopeId = ReadOnlySpan.Empty; diff --git a/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs b/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs index 9eb3dc5b2b6346..0cbcd07af69c0d 100644 --- a/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs +++ b/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs @@ -13,7 +13,7 @@ internal static partial class IPv4AddressHelper { // methods // Parse and canonicalize - internal static string ParseCanonicalName(in ReadOnlySpan str, ref bool isLoopback) + internal static string ParseCanonicalName(ReadOnlySpan str, ref bool isLoopback) { Span numbers = stackalloc byte[NumberOfLabels]; isLoopback = Parse(str, numbers); @@ -36,10 +36,10 @@ internal static string ParseCanonicalName(in ReadOnlySpan str, ref bool is // // Convert this IPv4 address into a sequence of 4 8-bit numbers // - private static bool Parse(in ReadOnlySpan name, Span numbers) + private static bool Parse(ReadOnlySpan name, Span numbers) { // "name" parameter includes ports, so charsConsumed may be different from span length - long result = ParseNonCanonical(in name, out _, true); + long result = ParseNonCanonical(name, out _, true); Debug.Assert(result != Invalid, $"Failed to parse after already validated: {name}"); diff --git a/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs b/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs index 448d8435ff55ab..617814f6431958 100644 --- a/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs +++ b/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs @@ -10,11 +10,11 @@ namespace System.Net // The idea is to stay with static helper methods and strings internal static partial class IPv6AddressHelper { - internal static string ParseCanonicalName(in ReadOnlySpan str, ref bool isLoopback, out ReadOnlySpan scopeId) + internal static string ParseCanonicalName(ReadOnlySpan str, ref bool isLoopback, out ReadOnlySpan scopeId) { Span numbers = stackalloc ushort[NumberOfLabels]; numbers.Clear(); - Parse(in str, numbers, out scopeId); + Parse(str, numbers, out scopeId); isLoopback = IsLoopback(numbers); // RFC 5952 Sections 4 & 5 - Compressed, lower case, with possible embedded IPv4 addresses. From cb1d208ffcf0b2aeb382e24518a20ff790e6c85e Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Fri, 12 Jul 2024 13:20:15 +0100 Subject: [PATCH 29/43] Code review * Reverted from Spans to pointers --- .../System/Net/IPv4AddressHelper.Common.cs | 46 ++++++++++--------- .../System/Net/IPv6AddressHelper.Common.cs | 23 +++++----- .../Net/Security/TargetHostNameHelper.cs | 16 +++++-- .../src/System/Net/IPAddressParser.cs | 20 ++++++-- .../UnitTests/Fakes/IPv4AddressHelper.cs | 8 +--- .../UnitTests/Fakes/IPv6AddressHelper.cs | 5 +- .../src/System/IPv4AddressHelper.cs | 12 +++-- .../src/System/IPv6AddressHelper.cs | 4 +- .../System.Private.Uri/src/System/Uri.cs | 12 ++--- 9 files changed, 86 insertions(+), 60 deletions(-) diff --git a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs index b2f0477ec17d40..380d2c93b32f75 100644 --- a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs @@ -52,6 +52,16 @@ internal static int ParseHostNumber(ReadOnlySpan str) // name // string containing possible IPv4 address // + // start + // offset in to start checking for IPv4 address + // + // end + // offset in of the last character we can touch in the check + // + // Outputs: + // end + // index of last character in we checked + // // allowIPv6 // enables parsing IPv4 addresses embedded in IPv6 address literals // @@ -61,10 +71,6 @@ internal static int ParseHostNumber(ReadOnlySpan str) // unknownScheme // the check is made on an unknown scheme (suppress IPv4 canonicalization) // - // Outputs: - // charsConsumed - // index of last character in we checked - // // Assumes: // The address string is terminated by either // end of the string, characters ':' '/' '\' '?' @@ -78,17 +84,17 @@ internal static int ParseHostNumber(ReadOnlySpan str) // //Remark: MUST NOT be used unless all input indexes are verified and trusted. - internal static bool IsValid(ReadOnlySpan name, out int charsConsumed, bool allowIPv6, bool notImplicitFile, bool unknownScheme) + internal static unsafe bool IsValid(TChar* name, int start, ref int end, bool allowIPv6, bool notImplicitFile, bool unknownScheme) where TChar : unmanaged, IBinaryInteger { // IPv6 can only have canonical IPv4 embedded. Unknown schemes will not attempt parsing of non-canonical IPv4 addresses. if (allowIPv6 || unknownScheme) { - return IsValidCanonical(name, out charsConsumed, allowIPv6, notImplicitFile); + return IsValidCanonical(name, start, ref end, allowIPv6, notImplicitFile); } else { - return ParseNonCanonical(name, out charsConsumed, notImplicitFile) != Invalid; + return ParseNonCanonical(name, start, ref end, notImplicitFile) != Invalid; } } @@ -105,21 +111,19 @@ internal static bool IsValid(ReadOnlySpan name, out int charsConsu // / "2" %x30-34 DIGIT ; 200-249 // / "25" %x30-35 ; 250-255 // - internal static bool IsValidCanonical(ReadOnlySpan name, out int charsConsumed, bool allowIPv6, bool notImplicitFile) + internal static unsafe bool IsValidCanonical(TChar* name, int start, ref int end, bool allowIPv6, bool notImplicitFile) where TChar : unmanaged, IBinaryInteger { int dots = 0; int number = 0; bool haveNumber = false; bool firstCharIsZero = false; - int current; Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); - charsConsumed = 0; - for (current = 0; current < name.Length; current++) + while (start < end) { - TChar ch = name[current]; + TChar ch = name[start]; if (allowIPv6) { @@ -144,7 +148,7 @@ internal static bool IsValidCanonical(ReadOnlySpan name, out int c // A number starting with zero should be interpreted in base 8 / octal if (!haveNumber && parsedCharacter == 0) { - if (current + 1 < name.Length && name[current + 1] == TChar.CreateTruncating('0')) + if ((start + 1 < end) && name[start + 1] == TChar.CreateTruncating('0')) { // 00 is not allowed as a prefix. return false; @@ -177,11 +181,12 @@ internal static bool IsValidCanonical(ReadOnlySpan name, out int c { return false; } + ++start; } bool res = (dots == 3) && haveNumber; if (res) { - charsConsumed = current; + end = start; } return res; } @@ -190,7 +195,7 @@ internal static bool IsValidCanonical(ReadOnlySpan name, out int c // Return Invalid (-1) for failures. // If the address has less than three dots, only the rightmost section is assumed to contain the combined value for // the missing sections: 0xFF00FFFF == 0xFF.0x00.0xFF.0xFF == 0xFF.0xFFFF - internal static long ParseNonCanonical(ReadOnlySpan name, out int charsConsumed, bool notImplicitFile) + internal static unsafe long ParseNonCanonical(TChar* name, int start, ref int end, bool notImplicitFile) where TChar : unmanaged, IBinaryInteger { int numberBase = IPv4AddressHelper.Decimal; @@ -204,8 +209,7 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); - charsConsumed = 0; - for (current = 0; current < name.Length; current++) + for (current = start; current < end; current++) { TChar ch = name[current]; int maxCharacterValue = '9'; @@ -219,7 +223,7 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int { current++; atLeastOneChar = true; - if (current < name.Length) + if (current < end) { ch = name[current]; @@ -239,7 +243,7 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int } // Parse this section - for (; current < name.Length; current++) + for (; current < end; current++) { ch = name[current]; int characterValue = int.CreateTruncating(ch); @@ -302,7 +306,7 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int { return Invalid; // Empty trailing segment: 1.1.1. } - else if (current >= name.Length) + else if (current >= end) { // end of string, allowed } @@ -320,7 +324,7 @@ internal static long ParseNonCanonical(ReadOnlySpan name, out int } parts[dotCount] = unchecked((uint)currentValue); - charsConsumed = current; + end = current; // Parsed, reassemble and check for overflows in the last part. Previous parts have already been checked in the loop switch (dotCount) diff --git a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs index 538f99044c0b03..48be83985863eb 100644 --- a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs @@ -96,7 +96,7 @@ internal static bool ShouldHaveIpv4Embedded(ReadOnlySpan numbers) // Remarks: MUST NOT be used unless all input indexes are verified and trusted. // start must be next to '[' position, or error is reported - internal static bool IsValidStrict(ReadOnlySpan name) + internal static unsafe bool IsValidStrict(TChar* name, int start, ref int end) where TChar : unmanaged, IBinaryInteger { // Number of components in this IPv6 address @@ -110,11 +110,10 @@ internal static bool IsValidStrict(ReadOnlySpan name) int lastSequence = 1; bool needsClosingBracket = false; - int start = 0; // An IPv6 address may begin with a start character ('['). If it does, it must end with an end // character (']'). - if (start < name.Length && name[start] == TChar.CreateTruncating('[')) + if (start < end && name[start] == TChar.CreateTruncating('[')) { start++; needsClosingBracket = true; @@ -122,17 +121,17 @@ internal static bool IsValidStrict(ReadOnlySpan name) // IsValidStrict() is only called if there is a ':' in the name string, i.e. // it is a possible IPv6 address. So, if the string starts with a '[' and // the pointer is advanced here there are still more characters to parse. - Debug.Assert(start < name.Length); + Debug.Assert(start < end); } // Starting with a colon character is only valid if another colon follows. - if (name[start] == TChar.CreateTruncating(':') && (start + 1 >= name.Length || name[start + 1] != TChar.CreateTruncating(':'))) + if (name[start] == TChar.CreateTruncating(':') && (start + 1 >= end || name[start + 1] != TChar.CreateTruncating(':'))) { return false; } int i; - for (i = start; i < name.Length; ++i) + for (i = start; i < end; ++i) { int hexCh = int.CreateTruncating(name[i]) | 0x20; @@ -162,7 +161,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) { bool moveToNextCharacter = true; - while (i + 1 < name.Length) + while (i + 1 < end) { i++; @@ -190,7 +189,7 @@ internal static bool IsValidStrict(ReadOnlySpan name) // If there's more after the closing bracket, it must be a port. // We don't use the port, but we still validate it. - if (i + 1 < name.Length && name[i + 1] != TChar.CreateTruncating(':')) + if (i + 1 < end && name[i + 1] != TChar.CreateTruncating(':')) { return false; } @@ -201,14 +200,14 @@ internal static bool IsValidStrict(ReadOnlySpan name) i += 2; // If there is a port, it must either be a hexadecimal or decimal number. // If the next two characters are '0x' or '0X' then it's a hexadecimal number. Skip the prefix. - if (i + 1 < name.Length && name[i] == TChar.CreateTruncating('0') && (name[i + 1] | TChar.CreateTruncating(0x20)) == TChar.CreateTruncating('x')) + if (i + 1 < end && name[i] == TChar.CreateTruncating('0') && (name[i + 1] | TChar.CreateTruncating(0x20)) == TChar.CreateTruncating('x')) { i += 2; numericBase = IPv6AddressHelper.Hex; } - for (; i < name.Length; i++) + for (; i < end; i++) { int ch = int.CreateTruncating(name[i]) | 0x20; @@ -261,11 +260,11 @@ internal static bool IsValidStrict(ReadOnlySpan name) return false; } - if (!IPv4AddressHelper.IsValid(name.Slice(lastSequence), out int ipv4AddressLength, true, false, false)) + i = end; + if (!IPv4AddressHelper.IsValid(name, lastSequence, ref i, true, false, false)) { return false; } - i = lastSequence + ipv4AddressLength; // ipv4 address takes 2 slots in ipv6 address, one was just counted meeting the '.' ++sequenceCount; lastSequence = i - sequenceLength; diff --git a/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs b/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs index 0ac5e9e8542eec..4767e1e4ff4908 100644 --- a/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs +++ b/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs @@ -40,7 +40,7 @@ internal static string NormalizeHostName(string? targetHost) // Simplified version of IPAddressParser.Parse to avoid allocations and dependencies. // It purposely ignores scopeId as we don't really use so we do not need to map it to actual interface id. - internal static bool IsValidAddress(string? hostname) + internal static unsafe bool IsValidAddress(string? hostname) { if (string.IsNullOrEmpty(hostname)) { @@ -49,17 +49,27 @@ internal static bool IsValidAddress(string? hostname) ReadOnlySpan ipSpan = hostname.AsSpan(); + int end = ipSpan.Length; + if (ipSpan.Contains(':')) { // The address is parsed as IPv6 if and only if it contains a colon. This is valid because // we don't support/parse a port specification at the end of an IPv4 address. Span numbers = stackalloc ushort[IPAddressParserStatics.IPv6AddressShorts]; - return IPv6AddressHelper.IsValidStrict(ipSpan); + fixed (char* ipStringPtr = &MemoryMarshal.GetReference(ipSpan)) + { + return IPv6AddressHelper.IsValidStrict(ipStringPtr, 0, ref end); + } } else if (char.IsDigit(ipSpan[0])) { - long tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipSpan, out int end, notImplicitFile: true); + long tmpAddr; + + fixed (char* ipStringPtr = &MemoryMarshal.GetReference(ipSpan)) + { + tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipStringPtr, 0, ref end, notImplicitFile: true); + } if (tmpAddr != IPv4AddressHelper.Invalid && end == ipSpan.Length) { diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs index 22808d92a474fe..e94e44ac0461f9 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs @@ -45,10 +45,16 @@ internal static class IPAddressParser throw new FormatException(SR.dns_bad_ip_address, new SocketException(SocketError.InvalidArgument)); } - private static bool TryParseIpv4(ReadOnlySpan ipSpan, out long address) + private static unsafe bool TryParseIpv4(ReadOnlySpan ipSpan, out long address) where TChar : unmanaged, IBinaryInteger { - long tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipSpan, out int end, notImplicitFile: true); + int end = ipSpan.Length; + long tmpAddr; + + fixed (TChar* ipStringPtr = &MemoryMarshal.GetReference(ipSpan)) + { + tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipStringPtr, 0, ref end, notImplicitFile: true); + } if (tmpAddr != IPv4AddressHelper.Invalid && end == ipSpan.Length) { @@ -63,14 +69,20 @@ private static bool TryParseIpv4(ReadOnlySpan ipSpan, out long add return false; } - private static bool TryParseIPv6(ReadOnlySpan ipSpan, Span numbers, int numbersLength, out uint scope) + private static unsafe bool TryParseIPv6(ReadOnlySpan ipSpan, Span numbers, int numbersLength, out uint scope) where TChar : unmanaged, IBinaryInteger { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); Debug.Assert(numbersLength >= IPAddressParserStatics.IPv6AddressShorts); + int end = ipSpan.Length; + bool isValid = false; + fixed (TChar* ipStringPtr = &MemoryMarshal.GetReference(ipSpan)) + { + isValid = IPv6AddressHelper.IsValidStrict(ipStringPtr, 0, ref end); + } scope = 0; - if (IPv6AddressHelper.IsValidStrict(ipSpan)) + if (isValid || (end != ipSpan.Length)) { IPv6AddressHelper.Parse(ipSpan, numbers, out ReadOnlySpan scopeIdSpan); diff --git a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs index 0810a29d6589f4..d260200c4d1489 100644 --- a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs +++ b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv4AddressHelper.cs @@ -8,11 +8,7 @@ namespace System.Net internal static class IPv4AddressHelper { internal const int Invalid = -1; - internal static unsafe long ParseNonCanonical(ReadOnlySpan name, out int bytesConsumed, bool notImplicitFile) - where TChar : unmanaged, IBinaryInteger - { - bytesConsumed = 0; - return 0; - } + internal static unsafe long ParseNonCanonical(TChar* name, int start, ref int end, bool notImplicitFile) + where TChar : unmanaged, IBinaryInteger => 0; } } diff --git a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs index 37262b6471b30c..32dc4cc2631925 100644 --- a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs +++ b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs @@ -11,9 +11,8 @@ internal static class IPv6AddressHelper internal static unsafe (int longestSequenceStart, int longestSequenceLength) FindCompressionRange( ReadOnlySpan numbers) => (-1, -1); internal static unsafe bool ShouldHaveIpv4Embedded(ReadOnlySpan numbers) => false; - internal static unsafe bool IsValidStrict(ReadOnlySpan name) - where TChar : unmanaged, IBinaryInteger - => false; + internal static unsafe bool IsValidStrict(TChar* name, int start, ref int end) + where TChar : unmanaged, IBinaryInteger => false; internal static unsafe bool Parse(ReadOnlySpan address, Span numbers, out ReadOnlySpan scopeId) where TChar : unmanaged, IBinaryInteger { diff --git a/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs b/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs index 0cbcd07af69c0d..6f8832513aaf11 100644 --- a/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs +++ b/src/libraries/System.Private.Uri/src/System/IPv4AddressHelper.cs @@ -4,6 +4,7 @@ using System.Buffers.Binary; using System.Diagnostics; using System.Numerics; +using System.Runtime.InteropServices; namespace System.Net { @@ -36,11 +37,16 @@ internal static string ParseCanonicalName(ReadOnlySpan str, ref bool isLoo // // Convert this IPv4 address into a sequence of 4 8-bit numbers // - private static bool Parse(ReadOnlySpan name, Span numbers) + private static unsafe bool Parse(ReadOnlySpan name, Span numbers) { - // "name" parameter includes ports, so charsConsumed may be different from span length - long result = ParseNonCanonical(name, out _, true); + int changedEnd = name.Length; + long result; + fixed (char* ipString = &MemoryMarshal.GetReference(name)) + { + // "name" parameter includes ports, so changedEnd may be different from span length + result = ParseNonCanonical(ipString, 0, ref changedEnd, true); + } Debug.Assert(result != Invalid, $"Failed to parse after already validated: {name}"); BinaryPrimitives.WriteUInt32BigEndian(numbers, (uint)result); diff --git a/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs b/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs index 617814f6431958..5637e552593a96 100644 --- a/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs +++ b/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs @@ -244,11 +244,11 @@ private static unsafe bool InternalIsValid(char* name, int start, ref int end, b return false; } - if (!IPv4AddressHelper.IsValid(new ReadOnlySpan(name + lastSequence, end - lastSequence), out int ipv4AddressLength, true, false, false)) + i = end; + if (!IPv4AddressHelper.IsValid(name, lastSequence, ref i, true, false, false)) { return false; } - i = lastSequence + ipv4AddressLength; // ipv4 address takes 2 slots in ipv6 address, one was just counted meeting the '.' ++sequenceCount; haveIPv4Address = true; diff --git a/src/libraries/System.Private.Uri/src/System/Uri.cs b/src/libraries/System.Private.Uri/src/System/Uri.cs index 791296e1c6237a..a5600ecfb2b10d 100644 --- a/src/libraries/System.Private.Uri/src/System/Uri.cs +++ b/src/libraries/System.Private.Uri/src/System/Uri.cs @@ -1281,11 +1281,12 @@ public static UriHostNameType CheckHostName(string? name) return UriHostNameType.IPv6; } } - } - if (IPv4AddressHelper.IsValid(name.AsSpan(), out end, false, false, false) && end == name.Length) - { - return UriHostNameType.IPv4; + end = name.Length; + if (IPv4AddressHelper.IsValid(fixedName, 0, ref end, false, false, false) && end == name.Length) + { + return UriHostNameType.IPv4; + } } if (DomainNameHelper.IsValid(name, iri: false, notImplicitFile: false, out int length) && length == name.Length) @@ -3861,9 +3862,8 @@ private unsafe int CheckAuthorityHelper(char* pString, int idx, int length, } } else if (char.IsAsciiDigit(ch) && syntax.InFact(UriSyntaxFlags.AllowIPv4Host) && - IPv4AddressHelper.IsValid(new ReadOnlySpan(pString + start, end - start), out int charsConsumed, false, StaticNotAny(flags, Flags.ImplicitFile), syntax.InFact(UriSyntaxFlags.V1_UnknownUri))) + IPv4AddressHelper.IsValid(pString, start, ref end, false, StaticNotAny(flags, Flags.ImplicitFile), syntax.InFact(UriSyntaxFlags.V1_UnknownUri))) { - end = start + charsConsumed; flags |= Flags.IPv4HostType; if (hasUnicode) From 0f3bbafda38a7de49c7ee01ca89ee873f25a8324 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Fri, 12 Jul 2024 16:36:19 +0100 Subject: [PATCH 30/43] Additional optimisations post-benchmark * Hoisted a condition out of its loop. * Switched from a series of comparisons when validating a hex digit to a simple table lookup. * Adjusted IPv6AddressHelper.Parse to eliminate range checks on the components. Nothing reaches .Parse without being validated, so there's no overflow risk here. --- .../System/Net/IPv4AddressHelper.Common.cs | 11 +--- .../System/Net/IPv6AddressHelper.Common.cs | 59 ++++++++----------- 2 files changed, 27 insertions(+), 43 deletions(-) diff --git a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs index 380d2c93b32f75..ef4ef2ba87454d 100644 --- a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs @@ -255,15 +255,8 @@ internal static unsafe long ParseNonCanonical(TChar* name, int start, ref } else if (numberBase == IPv4AddressHelper.Hex) { - if (characterValue >= 'a' && characterValue <= 'f') - { - digitValue = 10 + characterValue - 'a'; - } - else if (characterValue >= 'A' && characterValue <= 'F') - { - digitValue = 10 + characterValue - 'A'; - } - else + digitValue = HexConverter.FromChar(characterValue); + if (digitValue == 0xFF) { break; // Invalid/terminator } diff --git a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs index 48be83985863eb..bfb6d3a9e349c0 100644 --- a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs @@ -8,7 +8,6 @@ namespace System.Net { internal static partial class IPv6AddressHelper { - private const int Decimal = 10; private const int Hex = 16; private const int NumberOfLabels = 8; @@ -194,8 +193,6 @@ internal static unsafe bool IsValidStrict(TChar* name, int start, ref int return false; } - int numericBase = IPv6AddressHelper.Decimal; - // Skip past the closing bracket and the port separator. i += 2; // If there is a port, it must either be a hexadecimal or decimal number. @@ -204,23 +201,24 @@ internal static unsafe bool IsValidStrict(TChar* name, int start, ref int { i += 2; - numericBase = IPv6AddressHelper.Hex; - } - - for (; i < end; i++) - { - int ch = int.CreateTruncating(name[i]) | 0x20; - - if ((ch >= '0' && ch <= '9') - || (numericBase == IPv6AddressHelper.Hex - && ch >= 'a' - && ch <= 'f')) + for (; i < end; i++) { - continue; + int ch = int.CreateTruncating(name[i]); + + if (HexConverter.FromChar(ch) == 0xFF) + { + return false; + } } - else + } + else + { + for (; i < end; i++) { - return false; + if (name[i] < TChar.CreateTruncating('0') || name[i] > TChar.CreateTruncating('9')) + { + return false; + } } } continue; @@ -326,7 +324,7 @@ internal static unsafe bool IsValidStrict(TChar* name, int start, ref int internal static void Parse(ReadOnlySpan address, scoped Span numbers, out ReadOnlySpan scopeId) where TChar : unmanaged, IBinaryInteger { - int number = 0; + ushort number = 0; int index = 0; int compressorIndex = -1; bool numberIsValid = true; @@ -343,7 +341,7 @@ internal static void Parse(ReadOnlySpan address, scoped Span(ReadOnlySpan address, scoped Span(ReadOnlySpan address, scoped Span> 16); - numbers[index++] = (ushort)number; + int ipv4Address = IPv4AddressHelper.ParseHostNumber(address.Slice(i, j - i)); + + numbers[index++] = unchecked((ushort)(ipv4Address >> 16)); + numbers[index++] = unchecked((ushort)(ipv4Address & 0xFFFF)); i = j; // set this to avoid adding another number to @@ -426,17 +425,9 @@ internal static void Parse(ReadOnlySpan address, scoped Span 9)) - { - int lowercaseCharacterValue = characterValue | 0x20; - - digitValue = 10 + lowercaseCharacterValue - 'a'; - } + int castCharacter = int.CreateTruncating(ch); - number = number * IPv6AddressHelper.Hex + digitValue; + number = unchecked((ushort)(number * IPv6AddressHelper.Hex + HexConverter.FromChar(castCharacter))); } } @@ -444,7 +435,7 @@ internal static void Parse(ReadOnlySpan address, scoped Span Date: Mon, 15 Jul 2024 20:27:28 +0100 Subject: [PATCH 31/43] Performance improvement Mostly in the IPv6 parsing. * Removed HexConverter, which adds a little more latency than expected. * For short (<= 16) IPv6 addresses, ROS.Contains and ROS.IndexOf are more expensive than iterating over each character. --- .../System/Net/IPv4AddressHelper.Common.cs | 15 ++++++++---- .../System/Net/IPv6AddressHelper.Common.cs | 23 ++++++++++++------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs index ef4ef2ba87454d..6746ae6fe0dc65 100644 --- a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs @@ -32,7 +32,7 @@ internal static int ParseHostNumber(ReadOnlySpan str) for (; (start < str.Length) && (ch = int.CreateTruncating(str[start])) != '.' && ch != ':'; ++start) { - b = (b * 10) + ch - '0'; + b = unchecked((b * 10) + ch - '0'); } numbers[i] = (byte)b; @@ -255,8 +255,15 @@ internal static unsafe long ParseNonCanonical(TChar* name, int start, ref } else if (numberBase == IPv4AddressHelper.Hex) { - digitValue = HexConverter.FromChar(characterValue); - if (digitValue == 0xFF) + if (characterValue is >= 'a' and <= 'f') + { + digitValue = characterValue - 'a' + 10; + } + else if (characterValue is >= 'A' and <= 'F') + { + digitValue = characterValue - 'A' + 10; + } + else { break; // Invalid/terminator } @@ -266,7 +273,7 @@ internal static unsafe long ParseNonCanonical(TChar* name, int start, ref break; // Invalid/terminator } - currentValue = (currentValue * numberBase) + digitValue; + currentValue = unchecked((currentValue * numberBase) + digitValue); if (currentValue > MaxIPv4Value) // Overflow { diff --git a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs index bfb6d3a9e349c0..80f0a7da134b77 100644 --- a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs @@ -197,7 +197,7 @@ internal static unsafe bool IsValidStrict(TChar* name, int start, ref int i += 2; // If there is a port, it must either be a hexadecimal or decimal number. // If the next two characters are '0x' or '0X' then it's a hexadecimal number. Skip the prefix. - if (i + 1 < end && name[i] == TChar.CreateTruncating('0') && (name[i + 1] | TChar.CreateTruncating(0x20)) == TChar.CreateTruncating('x')) + if (i + 1 < end && name[i] == TChar.CreateTruncating('0') && (name[i + 1] == TChar.CreateTruncating('x') || name[i + 1] == TChar.CreateTruncating('X'))) { i += 2; @@ -205,7 +205,7 @@ internal static unsafe bool IsValidStrict(TChar* name, int start, ref int { int ch = int.CreateTruncating(name[i]); - if (HexConverter.FromChar(ch) == 0xFF) + if (ch is not (>= '0' and <= '9' or >= 'a' and <= 'f' or >= 'A' and <= 'F')) { return false; } @@ -328,13 +328,13 @@ internal static void Parse(ReadOnlySpan address, scoped Span.Empty; // Skip the start '[' character, if present. Stop parsing at the end IPv6 address terminator (']'). - for (int i = (address[0] == TChar.CreateTruncating('[') ? 1 : 0); i < end;) + for (int i = (address[0] == TChar.CreateTruncating('[') ? 1 : 0); i < address.Length && address[i] != TChar.CreateTruncating(']');) { if (address[i] == TChar.CreateTruncating('%') || address[i] == TChar.CreateTruncating('/')) @@ -379,7 +379,7 @@ internal static void Parse(ReadOnlySpan address, scoped Span(ReadOnlySpan address, scoped Span= '0' and <= '9' => castCharacter - '0', + >= 'a' and <= 'f' => castCharacter - 'a' + 10, + >= 'A' and <= 'F' => castCharacter - 'A' + 10, + _ => 0x0 + }; - number = unchecked((ushort)(number * IPv6AddressHelper.Hex + HexConverter.FromChar(castCharacter))); + number = unchecked((ushort)(number * IPv6AddressHelper.Hex + characterValue)); } } From 21f1f864e65778e1ee1305fe7e3865b4bc5a626d Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Wed, 17 Jul 2024 21:09:42 +0100 Subject: [PATCH 32/43] Changes following code review Style/comment changes to IPv4AddressHelper. Swapped IPv6AddressHelper back to the previous switch block. --- .../System/Net/IPv4AddressHelper.Common.cs | 46 ++- .../System/Net/IPv6AddressHelper.Common.cs | 391 +++++++++--------- 2 files changed, 220 insertions(+), 217 deletions(-) diff --git a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs index 6746ae6fe0dc65..4a1b4cd8fafe79 100644 --- a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs @@ -19,18 +19,19 @@ internal static partial class IPv4AddressHelper private const int NumberOfLabels = 4; // Only called from the IPv6Helper, only parse the canonical format - internal static int ParseHostNumber(ReadOnlySpan str) + internal static int ParseHostNumber(ReadOnlySpan str, int start, int end) where TChar : unmanaged, IBinaryInteger { + Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); + Span numbers = stackalloc byte[NumberOfLabels]; - int start = 0; for (int i = 0; i < numbers.Length; ++i) { int b = 0; int ch; - for (; (start < str.Length) && (ch = int.CreateTruncating(str[start])) != '.' && ch != ':'; ++start) + for (; (start < end) && (ch = int.CreateTruncating(str[start])) != '.' && ch != ':'; ++start) { b = unchecked((b * 10) + ch - '0'); } @@ -114,13 +115,13 @@ internal static unsafe bool IsValid(TChar* name, int start, ref int end, internal static unsafe bool IsValidCanonical(TChar* name, int start, ref int end, bool allowIPv6, bool notImplicitFile) where TChar : unmanaged, IBinaryInteger { + Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); + int dots = 0; int number = 0; bool haveNumber = false; bool firstCharIsZero = false; - Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); - while (start < end) { TChar ch = name[start]; @@ -133,11 +134,12 @@ internal static unsafe bool IsValidCanonical(TChar* name, int start, ref break; } } - // For a normal IPv4 address, the terminator is the prefix ('/' or its counterpart, '\'). If notImplicitFile is set, the terminator - // is one of the characters which signify the start of the rest of the URI - the port number (':'), query string ('?') or fragment ('#') else if (ch == TChar.CreateTruncating('/') || ch == TChar.CreateTruncating('\\') || (notImplicitFile && (ch == TChar.CreateTruncating(':') || ch == TChar.CreateTruncating('?') || ch == TChar.CreateTruncating('#')))) { + // For a normal IPv4 address, the terminator is the prefix ('/' or its counterpart, '\'). If notImplicitFile is set, the terminator + // is one of the characters which signify the start of the rest of the URI - the port number (':'), query string ('?') or fragment ('#') + break; } @@ -164,9 +166,10 @@ internal static unsafe bool IsValidCanonical(TChar* name, int start, ref return false; } } - // If the current character is not an integer, it may be the IPv4 component separator ('.') else if (ch == TChar.CreateTruncating('.')) { + // If the current character is not an integer, it may be the IPv4 component separator ('.') + if (!haveNumber || (number > 0 && firstCharIsZero)) { // 0 is not allowed to prefix a number. @@ -198,27 +201,28 @@ internal static unsafe bool IsValidCanonical(TChar* name, int start, ref internal static unsafe long ParseNonCanonical(TChar* name, int start, ref int end, bool notImplicitFile) where TChar : unmanaged, IBinaryInteger { + Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); + int numberBase = IPv4AddressHelper.Decimal; - Span parts = stackalloc uint[4]; + uint* parts = stackalloc uint[4]; long currentValue = 0; bool atLeastOneChar = false; // Parse one dotted section at a time int dotCount = 0; // Limit 3 - int current; + int current = start; - Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); - - for (current = start; current < end; current++) + for (; current < end; current++) { TChar ch = name[current]; int maxCharacterValue = '9'; currentValue = 0; - // Figure out what base this section is in, default to base 10 - numberBase = IPv4AddressHelper.Decimal; + // Figure out what base this section is in, default to base 10. // A number starting with zero should be interpreted in base 8 / octal // If the number starts with 0x, it should be interpreted in base 16 / hex + numberBase = IPv4AddressHelper.Decimal; + if (ch == TChar.CreateTruncating('0')) { current++; @@ -227,7 +231,7 @@ internal static unsafe long ParseNonCanonical(TChar* name, int start, ref { ch = name[current]; - if ((ch == TChar.CreateTruncating('x')) || (ch == TChar.CreateTruncating('X'))) + if (ch == TChar.CreateTruncating('x') || ch == TChar.CreateTruncating('X')) { numberBase = IPv4AddressHelper.Hex; @@ -283,7 +287,7 @@ internal static unsafe long ParseNonCanonical(TChar* name, int start, ref atLeastOneChar = true; } - if (ch == TChar.CreateTruncating('.')) + if (current < end && ch == TChar.CreateTruncating('.')) { if (dotCount >= 3 // Max of 3 dots and 4 segments || !atLeastOneChar // No empty segments: 1...1 @@ -310,12 +314,13 @@ internal static unsafe long ParseNonCanonical(TChar* name, int start, ref { // end of string, allowed } - // For a normal IPv4 address, the terminator is the prefix ('/' or its counterpart, '\'). If notImplicitFile is set, the terminator - // is one of the characters which signify the start of the rest of the URI - the port number (':'), query string ('?') or fragment ('#') else if (name[current] == TChar.CreateTruncating('/') || name[current] == TChar.CreateTruncating('\\') || (notImplicitFile && (name[current] == TChar.CreateTruncating(':') || name[current] == TChar.CreateTruncating('?') || name[current] == TChar.CreateTruncating('#')))) { - // end of string, (as terminated) allowed + // For a normal IPv4 address, the terminator is the prefix ('/' or its counterpart, '\'). If notImplicitFile is set, the terminator + // is one of the characters which signify the start of the rest of the URI - the port number (':'), query string ('?') or fragment ('#') + + end = current; } else { @@ -324,7 +329,6 @@ internal static unsafe long ParseNonCanonical(TChar* name, int start, ref } parts[dotCount] = unchecked((uint)currentValue); - end = current; // Parsed, reassemble and check for overflows in the last part. Previous parts have already been checked in the loop switch (dotCount) diff --git a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs index 80f0a7da134b77..4ab4c921dd00f2 100644 --- a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs @@ -98,6 +98,8 @@ internal static bool ShouldHaveIpv4Embedded(ReadOnlySpan numbers) internal static unsafe bool IsValidStrict(TChar* name, int start, ref int end) where TChar : unmanaged, IBinaryInteger { + Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); + // Number of components in this IPv6 address int sequenceCount = 0; // Length of the component currently being constructed @@ -132,10 +134,11 @@ internal static unsafe bool IsValidStrict(TChar* name, int start, ref int int i; for (i = start; i < end; ++i) { - int hexCh = int.CreateTruncating(name[i]) | 0x20; + int currentCh = int.CreateTruncating(name[i]); + int lowercaseCh = currentCh | 0x20; - if ((hexCh >= '0' && hexCh <= '9') - || (hexCh >= 'a' && hexCh <= 'f')) + if ((lowercaseCh >= '0' && lowercaseCh <= '9') + || (lowercaseCh >= 'a' && lowercaseCh <= 'f')) { ++sequenceLength; expectingNumber = false; @@ -153,127 +156,113 @@ internal static unsafe bool IsValidStrict(TChar* name, int start, ref int sequenceLength = 0; } - // An IPv6 address is separated from its scope by a '%' character. The scope - // is terminated by the natural end of the address, the address end character (']') - // or the start of the prefix ('/'). - if (name[i] == TChar.CreateTruncating('%')) + switch (currentCh) { - bool moveToNextCharacter = true; - - while (i + 1 < end) - { - i++; - - if (name[i] == TChar.CreateTruncating(']') - || name[i] == TChar.CreateTruncating('/')) + case '%': + // An IPv6 address is separated from its scope by a '%' character. The scope + // is terminated by the natural end of the address, the address end character (']') + // or the start of the prefix ('/'). + while (i + 1 < end) { - moveToNextCharacter = false; - break; + i++; + if (name[i] == TChar.CreateTruncating(']')) + { + goto case ']'; + } + else if (name[i] == TChar.CreateTruncating('/')) + { + goto case '/'; + } } - } - - if (moveToNextCharacter) - { - continue; - } - } - - if (name[i] == TChar.CreateTruncating(']')) - { - if (!needsClosingBracket) - { - return false; - } - needsClosingBracket = false; + break; - // If there's more after the closing bracket, it must be a port. - // We don't use the port, but we still validate it. - if (i + 1 < end && name[i + 1] != TChar.CreateTruncating(':')) - { - return false; - } + case ']': + if (!needsClosingBracket) + { + return false; + } + needsClosingBracket = false; - // Skip past the closing bracket and the port separator. - i += 2; - // If there is a port, it must either be a hexadecimal or decimal number. - // If the next two characters are '0x' or '0X' then it's a hexadecimal number. Skip the prefix. - if (i + 1 < end && name[i] == TChar.CreateTruncating('0') && (name[i + 1] == TChar.CreateTruncating('x') || name[i + 1] == TChar.CreateTruncating('X'))) - { - i += 2; + // If there's more after the closing bracket, it must be a port. + // We don't use the port, but we still validate it. + if (i + 1 < end && name[i + 1] != TChar.CreateTruncating(':')) + { + return false; + } - for (; i < end; i++) + // If there is a port, it must either be a hexadecimal or decimal number. + // If the next two characters are '0x' or '0X' then it's a hexadecimal number. Skip the prefix. + if (i + 3 < end && name[i + 2] == TChar.CreateTruncating('0') && (name[i + 3] == TChar.CreateTruncating('x') || name[i + 3] == TChar.CreateTruncating('X'))) { - int ch = int.CreateTruncating(name[i]); + i += 4; + for (; i < end; i++) + { + int ch = int.CreateTruncating(name[i]); - if (ch is not (>= '0' and <= '9' or >= 'a' and <= 'f' or >= 'A' and <= 'F')) + if (ch is not (>= '0' and <= '9' or >= 'a' and <= 'f' or >= 'A' and <= 'F')) + { + return false; + } + } + } + else + { + i += 2; + for (; i < end; i++) { - return false; + if (name[i] < TChar.CreateTruncating('0') || name[i] > TChar.CreateTruncating('9')) + { + return false; + } } } - } - else - { - for (; i < end; i++) + continue; + + case ':': + // If the next character after a colon is another colon, the address contains a compressor ('::'). + if ((i > 0) && (name[i - 1] == TChar.CreateTruncating(':'))) { - if (name[i] < TChar.CreateTruncating('0') || name[i] > TChar.CreateTruncating('9')) + if (haveCompressor) { + // can only have one per IPv6 address return false; } + haveCompressor = true; + expectingNumber = false; } - } - continue; - } - // A prefix in an IPv6 address is invalid. - else if (name[i] == TChar.CreateTruncating('/')) - { - return false; - } - // IPv6 address components are separated by at least one colon. - else if (name[i] == TChar.CreateTruncating(':')) - { - // If the next character after a colon is another colon, the address contains a compressor ('::'). - if (i > 0 && name[i - 1] == TChar.CreateTruncating(':')) - { - if (haveCompressor) + else { - // can only have one per IPv6 address - return false; + expectingNumber = true; } - haveCompressor = true; - expectingNumber = false; - } - else - { - expectingNumber = true; - } + break; - sequenceLength = 0; - continue; - } - // Encountering a '.' indicates that an IPv6 address may contain an embedded IPv4 address. - else if (name[i] == TChar.CreateTruncating('.')) - { - if (haveIPv4Address) - { + case '/': + // A prefix in an IPv6 address is invalid. return false; - } - i = end; - if (!IPv4AddressHelper.IsValid(name, lastSequence, ref i, true, false, false)) - { - return false; - } - // ipv4 address takes 2 slots in ipv6 address, one was just counted meeting the '.' - ++sequenceCount; - lastSequence = i - sequenceLength; - sequenceLength = 0; - haveIPv4Address = true; - --i; // it will be incremented back on the next loop + case '.': + if (haveIPv4Address) + { + return false; + } - continue; - } + i = end; + if (!IPv4AddressHelper.IsValid(name, lastSequence, ref i, true, false, false)) + { + return false; + } + // An IPv4 address takes 2 slots in an IPv6 address. One was just counted meeting the '.' + ++sequenceCount; + lastSequence = i - sequenceLength; + sequenceLength = 0; + haveIPv4Address = true; + --i; // it will be incremented back on the next loop + break; - return false; + default: + return false; + } + sequenceLength = 0; } } @@ -287,7 +276,7 @@ internal static unsafe bool IsValidStrict(TChar* name, int start, ref int ++sequenceCount; } - // these sequence counts are -1 because it is implied in end-of-sequence + // These sequence counts are -1 because it is implied in end-of-sequence. const int ExpectedSequenceCount = 8; return @@ -324,140 +313,150 @@ internal static unsafe bool IsValidStrict(TChar* name, int start, ref int internal static void Parse(ReadOnlySpan address, scoped Span numbers, out ReadOnlySpan scopeId) where TChar : unmanaged, IBinaryInteger { + Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); + ushort number = 0; + int currentCh; int index = 0; int compressorIndex = -1; bool numberIsValid = true; - // It's possible for an IPv6 address to be short enough that the cost of Contains outweighs the benefit of short-circuiting the parsing logic. - bool shortAddress = address.Length <= 16; - bool mayContainIPv4Separator = shortAddress || address.Contains(TChar.CreateTruncating('.')); scopeId = ReadOnlySpan.Empty; // Skip the start '[' character, if present. Stop parsing at the end IPv6 address terminator (']'). for (int i = (address[0] == TChar.CreateTruncating('[') ? 1 : 0); i < address.Length && address[i] != TChar.CreateTruncating(']');) { - if (address[i] == TChar.CreateTruncating('%') - || address[i] == TChar.CreateTruncating('/')) + currentCh = int.CreateTruncating(address[i]); + + switch (currentCh) { - if (numberIsValid) - { - numbers[index++] = number; - numberIsValid = false; - } + case '%': + if (numberIsValid) + { + numbers[index++] = number; + numberIsValid = false; + } - // The scope follows a '%' and terminates at the natural end of the address, the address terminator (']') or the prefix delimiter ('/'). - if (address[i] == TChar.CreateTruncating('%')) - { + // The scope follows a '%' and terminates at the natural end of the address, the address terminator (']') or the prefix delimiter ('/'). int scopeStart = i; for (++i; i < address.Length && address[i] != TChar.CreateTruncating(']') && address[i] != TChar.CreateTruncating('/'); ++i) { } scopeId = address.Slice(scopeStart, i - scopeStart); - } - // ignore prefix if any - for (; i < address.Length && address[i] != TChar.CreateTruncating(']'); ++i) - { - } - } - // IPv6 address components are separated by at least one colon. - else if (address[i] == TChar.CreateTruncating(':')) - { - numbers[index++] = number; - number = 0; - // Two sequential colons form a compressor ('::'). - ++i; - if (i < address.Length && address[i] == TChar.CreateTruncating(':')) - { - compressorIndex = index; - ++i; - } - else if ((compressorIndex < 0) && (index < 6)) - { - // No point checking for IPv4 address if we don't - // have a compressor or we haven't seen 6 16-bit - // numbers yet. - continue; - } - else if (!mayContainIPv4Separator) - { - // No point checking for IPv4 address if the string - // doesn't contain an IPv4 component separator. - continue; - } - // check to see if the upcoming number is really an IPv4 - // address. If it is, convert it to 2 ushort numbers - for (int j = i; j < address.Length && - (address[j] != TChar.CreateTruncating(']')) && - (address[j] != TChar.CreateTruncating(':')) && - (address[j] != TChar.CreateTruncating('%')) && - (address[j] != TChar.CreateTruncating('/')) && - (j < i + 4); ++j) - { + // Ignore the prefix (if any.) + for (; i < address.Length && address[i] != TChar.CreateTruncating(']'); ++i) + { + } + break; + + case ':': + numbers[index++] = number; + number = 0; + // Two sequential colons form a compressor ('::'). + ++i; + if (i < address.Length && address[i] == TChar.CreateTruncating(':')) + { + compressorIndex = index; + ++i; + } + else if ((compressorIndex < 0) && (index < 6)) + { + // No point checking for IPv4 address if we don't + // have a compressor or we haven't seen 6 16-bit + // numbers yet. + break; + } - if (address[j] == TChar.CreateTruncating('.')) + // Check to see if the upcoming number is really an IPv4 + // address. If it is, convert it to 2 ushort numbers + for (int j = i; j < address.Length && + (address[j] != TChar.CreateTruncating(']')) && + (address[j] != TChar.CreateTruncating(':')) && + (address[j] != TChar.CreateTruncating('%')) && + (address[j] != TChar.CreateTruncating('/')) && + (j < i + 4); ++j) { - // We have an IPv4 address. Find the end of it: - // we know that since we have a valid IPv6 - // address, the only things that will terminate - // the IPv4 address are the prefix delimiter '/' - // or the end-of-string (which we conveniently - // delimited with ']'). - while (j < address.Length && (address[j] != TChar.CreateTruncating(']')) && (address[j] != TChar.CreateTruncating('/')) && (address[j] != TChar.CreateTruncating('%'))) + + if (address[j] == TChar.CreateTruncating('.')) { - ++j; - } - int ipv4Address = IPv4AddressHelper.ParseHostNumber(address.Slice(i, j - i)); + // We have an IPv4 address. Find the end of it: + // we know that since we have a valid IPv6 + // address, the only things that will terminate + // the IPv4 address are the prefix delimiter '/' + // or the end-of-string (which we conveniently + // delimited with ']'). + while (j < address.Length && (address[j] != TChar.CreateTruncating(']')) && (address[j] != TChar.CreateTruncating('/')) && (address[j] != TChar.CreateTruncating('%'))) + { + ++j; + } + int ipv4Address = IPv4AddressHelper.ParseHostNumber(address, i, j); - numbers[index++] = unchecked((ushort)(ipv4Address >> 16)); - numbers[index++] = unchecked((ushort)(ipv4Address & 0xFFFF)); - i = j; + numbers[index++] = unchecked((ushort)(ipv4Address >> 16)); + numbers[index++] = unchecked((ushort)(ipv4Address & 0xFFFF)); + i = j; + + // Set this to avoid adding another number to + // the array if there's a prefix + number = 0; + numberIsValid = false; + break; + } + } + break; - // set this to avoid adding another number to - // the array if there's a prefix - number = 0; + case '/': + if (numberIsValid) + { + numbers[index++] = number; numberIsValid = false; - break; } - } - } - else - { - TChar ch = address[i++]; - int castCharacter = int.CreateTruncating(ch); - int characterValue = castCharacter switch - { - >= '0' and <= '9' => castCharacter - '0', - >= 'a' and <= 'f' => castCharacter - 'a' + 10, - >= 'A' and <= 'F' => castCharacter - 'A' + 10, - _ => 0x0 - }; - number = unchecked((ushort)(number * IPv6AddressHelper.Hex + characterValue)); + for (++i; i < address.Length && address[i] != TChar.CreateTruncating(']'); i++) + { + } + + break; + + default: + TChar ch = address[i++]; + int castCharacter = int.CreateTruncating(ch); + int characterValue = castCharacter switch + { + >= '0' and <= '9' => castCharacter - '0', + >= 'a' and <= 'f' => castCharacter - 'a' + 10, + >= 'A' and <= 'F' => castCharacter - 'A' + 10, + _ => 0x0 + }; + + number = unchecked((ushort)(number * IPv6AddressHelper.Hex + characterValue)); + break; } } - // add number to the array if its not the prefix length or part of + // Add number to the array if it's not the prefix length or part of // an IPv4 address that's already been handled if (numberIsValid) { numbers[index++] = number; } - // if we had a compressor sequence ("::") then we need to expand the - // numbers array - // If index is the same as NumberOfLabels, it means that "zero bits" are already in the correct place. - // It happens for leading and trailing compression. - if (compressorIndex > 0 && index != NumberOfLabels) + // If we had a compressor sequence ("::") then we need to expand the + // numbers array. + if (compressorIndex > 0) { int toIndex = NumberOfLabels - 1; int fromIndex = index - 1; - for (int i = index - compressorIndex; i > 0; --i) + // If fromIndex and toIndex are the same, it means that "zero bits" are already in the correct place. + // This happens for leading and trailing compression. + if (fromIndex != toIndex) { - numbers[toIndex--] = numbers[fromIndex]; - numbers[fromIndex--] = 0; + for (int i = index - compressorIndex; i > 0; --i) + { + numbers[toIndex--] = numbers[fromIndex]; + numbers[fromIndex--] = 0; + } } } } From 70a60a506aae6fd92885f56d5ff6e27dfa4efdc6 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 20 Jul 2024 17:46:54 +0100 Subject: [PATCH 33/43] Initial response to newest review * Removed references to unchecked. * Changed numeric character detection to align with char.IsAsciiDigit. * Changed hex character detection to use HexConverter. * Cosmetic change: reordered if-condition and changed comment to match - slightly cleans up diff. * Cosmetic change: line spacing. --- .../System/Net/IPv4AddressHelper.Common.cs | 45 +++++-------------- .../System/Net/IPv6AddressHelper.Common.cs | 25 ++++------- .../src/System/Net/IPAddressParser.cs | 4 +- 3 files changed, 23 insertions(+), 51 deletions(-) diff --git a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs index 4a1b4cd8fafe79..2f4e1dd50d813b 100644 --- a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs @@ -33,7 +33,7 @@ internal static int ParseHostNumber(ReadOnlySpan str, int start, i for (; (start < end) && (ch = int.CreateTruncating(str[start])) != '.' && ch != ':'; ++start) { - b = unchecked((b * 10) + ch - '0'); + b = (b * 10) + ch - '0'; } numbers[i] = (byte)b; @@ -128,8 +128,8 @@ internal static unsafe bool IsValidCanonical(TChar* name, int start, ref if (allowIPv6) { - // For an IPv4 address nested inside an IPv6, the terminator is either ScopeId ('%'), prefix ('/') or ipv6 address terminator (']') - if (ch == TChar.CreateTruncating('%') || ch == TChar.CreateTruncating('/') || ch == TChar.CreateTruncating(']')) + // For an IPv4 address nested inside an IPv6 address, the terminator is either the IPv6 address terminator (']'), prefix ('/') or ScopeId ('%') + if (ch == TChar.CreateTruncating(']') || ch == TChar.CreateTruncating('/') || ch == TChar.CreateTruncating('%')) { break; } @@ -143,9 +143,10 @@ internal static unsafe bool IsValidCanonical(TChar* name, int start, ref break; } - int parsedCharacter = int.CreateTruncating(ch) - '0'; + // An explicit cast to an unsigned integer forces character values preceding '0' to underflow, eliminating one comparison below. + uint parsedCharacter = uint.CreateTruncating(ch - TChar.CreateTruncating('0')); - if (parsedCharacter >= 0 && parsedCharacter <= 9) + if (parsedCharacter < IPv4AddressHelper.Decimal) { // A number starting with zero should be interpreted in base 8 / octal if (!haveNumber && parsedCharacter == 0) @@ -160,7 +161,7 @@ internal static unsafe bool IsValidCanonical(TChar* name, int start, ref } haveNumber = true; - number = number * Decimal + parsedCharacter; + number = number * IPv4AddressHelper.Decimal + (int)parsedCharacter; if (number > byte.MaxValue) { return false; @@ -215,7 +216,6 @@ internal static unsafe long ParseNonCanonical(TChar* name, int start, ref for (; current < end; current++) { TChar ch = name[current]; - int maxCharacterValue = '9'; currentValue = 0; // Figure out what base this section is in, default to base 10. @@ -241,7 +241,6 @@ internal static unsafe long ParseNonCanonical(TChar* name, int start, ref else { numberBase = IPv4AddressHelper.Octal; - maxCharacterValue = '7'; } } } @@ -251,33 +250,13 @@ internal static unsafe long ParseNonCanonical(TChar* name, int start, ref { ch = name[current]; int characterValue = int.CreateTruncating(ch); - int digitValue; + int digitValue = HexConverter.FromChar(characterValue); - if (characterValue >= '0' && characterValue <= maxCharacterValue) - { - digitValue = characterValue - '0'; - } - else if (numberBase == IPv4AddressHelper.Hex) - { - if (characterValue is >= 'a' and <= 'f') - { - digitValue = characterValue - 'a' + 10; - } - else if (characterValue is >= 'A' and <= 'F') - { - digitValue = characterValue - 'A' + 10; - } - else - { - break; // Invalid/terminator - } - } - else + if (digitValue >= numberBase) { break; // Invalid/terminator } - - currentValue = unchecked((currentValue * numberBase) + digitValue); + currentValue = (currentValue * numberBase) + digitValue; if (currentValue > MaxIPv4Value) // Overflow { @@ -296,7 +275,7 @@ internal static unsafe long ParseNonCanonical(TChar* name, int start, ref { return Invalid; } - parts[dotCount] = unchecked((uint)currentValue); + parts[dotCount] = (uint)currentValue; dotCount++; atLeastOneChar = false; continue; @@ -328,7 +307,7 @@ internal static unsafe long ParseNonCanonical(TChar* name, int start, ref return Invalid; } - parts[dotCount] = unchecked((uint)currentValue); + parts[dotCount] = (uint)currentValue; // Parsed, reassemble and check for overflows in the last part. Previous parts have already been checked in the loop switch (dotCount) diff --git a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs index 4ab4c921dd00f2..fd9bd005e16e84 100644 --- a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs @@ -8,6 +8,7 @@ namespace System.Net { internal static partial class IPv6AddressHelper { + private const int Decimal = 10; private const int Hex = 16; private const int NumberOfLabels = 8; @@ -135,10 +136,8 @@ internal static unsafe bool IsValidStrict(TChar* name, int start, ref int for (i = start; i < end; ++i) { int currentCh = int.CreateTruncating(name[i]); - int lowercaseCh = currentCh | 0x20; - if ((lowercaseCh >= '0' && lowercaseCh <= '9') - || (lowercaseCh >= 'a' && lowercaseCh <= 'f')) + if (HexConverter.IsHexChar(currentCh)) { ++sequenceLength; expectingNumber = false; @@ -199,7 +198,7 @@ internal static unsafe bool IsValidStrict(TChar* name, int start, ref int { int ch = int.CreateTruncating(name[i]); - if (ch is not (>= '0' and <= '9' or >= 'a' and <= 'f' or >= 'A' and <= 'F')) + if (!HexConverter.IsHexChar(ch)) { return false; } @@ -210,7 +209,7 @@ internal static unsafe bool IsValidStrict(TChar* name, int start, ref int i += 2; for (; i < end; i++) { - if (name[i] < TChar.CreateTruncating('0') || name[i] > TChar.CreateTruncating('9')) + if (uint.CreateTruncating(name[i] - TChar.CreateTruncating('0')) >= IPv6AddressHelper.Decimal) { return false; } @@ -355,7 +354,7 @@ internal static void Parse(ReadOnlySpan address, scoped Span(ReadOnlySpan address, scoped Span> 16)); - numbers[index++] = unchecked((ushort)(ipv4Address & 0xFFFF)); + numbers[index++] = (ushort)(ipv4Address >> 16); + numbers[index++] = (ushort)(ipv4Address & 0xFFFF); i = j; // Set this to avoid adding another number to @@ -421,15 +420,9 @@ internal static void Parse(ReadOnlySpan address, scoped Span= '0' and <= '9' => castCharacter - '0', - >= 'a' and <= 'f' => castCharacter - 'a' + 10, - >= 'A' and <= 'F' => castCharacter - 'A' + 10, - _ => 0x0 - }; + int characterValue = HexConverter.FromChar(castCharacter); - number = unchecked((ushort)(number * IPv6AddressHelper.Hex + characterValue)); + number = (ushort)(number * IPv6AddressHelper.Hex + characterValue); break; } } diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs index e94e44ac0461f9..6ddde36e1fdeb3 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs @@ -81,6 +81,7 @@ private static unsafe bool TryParseIPv6(ReadOnlySpan ipSpan, Span< { isValid = IPv6AddressHelper.IsValidStrict(ipStringPtr, 0, ref end); } + scope = 0; if (isValid || (end != ipSpan.Length)) { @@ -103,8 +104,7 @@ private static unsafe bool TryParseIPv6(ReadOnlySpan ipSpan, Span< interfaceName = System.Text.Encoding.UTF8.GetString(castScopeIdSpan); } - - if (typeof(TChar) == typeof(char)) + else if (typeof(TChar) == typeof(char)) { ReadOnlySpan castScopeIdSpan = MemoryMarshal.Cast(scopeIdSpan); From e0c49c2b7f80ac2d6b6e3bef3db7387a956a40f9 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 20 Jul 2024 19:09:21 +0100 Subject: [PATCH 34/43] Reverted hexadecimal prefix to be case-sensitive --- .../Common/src/System/Net/IPv6AddressHelper.Common.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs index fd9bd005e16e84..2d75b24258acc7 100644 --- a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs @@ -190,8 +190,8 @@ internal static unsafe bool IsValidStrict(TChar* name, int start, ref int } // If there is a port, it must either be a hexadecimal or decimal number. - // If the next two characters are '0x' or '0X' then it's a hexadecimal number. Skip the prefix. - if (i + 3 < end && name[i + 2] == TChar.CreateTruncating('0') && (name[i + 3] == TChar.CreateTruncating('x') || name[i + 3] == TChar.CreateTruncating('X'))) + // If the next two characters are '0x' then it's a hexadecimal number. Skip the prefix. + if (i + 3 < end && name[i + 2] == TChar.CreateTruncating('0') && name[i + 3] == TChar.CreateTruncating('x')) { i += 4; for (; i < end; i++) From 94b2dd1c1fbb9bec94f5cb1394e8d70ce239d56a Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sun, 21 Jul 2024 20:22:18 +0100 Subject: [PATCH 35/43] Flowed Spans through to PInvoke --- .../Interop.InterfaceNameToIndex.cs | 3 + .../IpHlpApi/Interop.if_nametoindex.cs | 3 + .../InterfaceInfoPal.Browser.cs | 12 +++ .../InterfaceInfoPal.Unix.cs | 52 +++++++++++ .../InterfaceInfoPal.Windows.cs | 89 +++++++++++++++++++ .../src/System.Net.Primitives.csproj | 4 + .../src/System/Net/IPAddressParser.cs | 11 ++- .../tests/FunctionalTests/IPAddressParsing.cs | 32 +++++-- .../System.Net.Primitives.Pal.Tests.csproj | 4 + ...stem.Net.Primitives.UnitTests.Tests.csproj | 4 + 10 files changed, 201 insertions(+), 13 deletions(-) diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.InterfaceNameToIndex.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.InterfaceNameToIndex.cs index 6dd5bf21e39596..1d7986480b5d8f 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.InterfaceNameToIndex.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.InterfaceNameToIndex.cs @@ -10,5 +10,8 @@ internal static partial class Sys { [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_InterfaceNameToIndex", StringMarshalling = StringMarshalling.Utf8, SetLastError = true)] public static partial uint InterfaceNameToIndex(string name); + + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_InterfaceNameToIndex", SetLastError = true)] + public static partial uint InterfaceNameToIndex(ReadOnlySpan utf8NullTerminatedName); } } diff --git a/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.if_nametoindex.cs b/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.if_nametoindex.cs index c17641eab29f85..ee311a26584204 100644 --- a/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.if_nametoindex.cs +++ b/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.if_nametoindex.cs @@ -10,5 +10,8 @@ internal static partial class IpHlpApi { [LibraryImport(Interop.Libraries.IpHlpApi, SetLastError = true)] internal static partial uint if_nametoindex([MarshalAs(UnmanagedType.LPStr)] string name); + + [LibraryImport(Interop.Libraries.IpHlpApi, SetLastError = true)] + internal static partial uint if_nametoindex(ReadOnlySpan ansiNullTerminatedName); } } diff --git a/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Browser.cs b/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Browser.cs index 46ccc26122da77..338930b3cfccf7 100644 --- a/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Browser.cs +++ b/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Browser.cs @@ -10,5 +10,17 @@ public static uint InterfaceNameToIndex(string _/*interfaceName*/) // zero means "unknown" return 0; } + + public static uint InterfaceNameToIndex(ReadOnlySpan _/*interfaceName*/) + { + // zero means "unknown" + return 0; + } + + public static uint InterfaceNameToIndex(ReadOnlySpan _/*interfaceName*/) + { + // zero means "unknown" + return 0; + } } } diff --git a/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Unix.cs b/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Unix.cs index fc537f3add6796..5ded8532c33c60 100644 --- a/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Unix.cs +++ b/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Unix.cs @@ -1,13 +1,65 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Runtime.InteropServices; +using System.Text; + namespace System.Net.NetworkInformation { internal static class InterfaceInfoPal { + private const int StackAllocationThreshold = 512; + public static uint InterfaceNameToIndex(string interfaceName) { return Interop.Sys.InterfaceNameToIndex(interfaceName); } + + public static unsafe uint InterfaceNameToIndex(ReadOnlySpan interfaceName) + { + // Includes null terminator. + int bufferSize = Encoding.UTF8.GetByteCount(interfaceName) + 1; + byte* nativeMemory = bufferSize <= StackAllocationThreshold ? null : (byte*)NativeMemory.Alloc((nuint)bufferSize); + + try + { + Span buffer = nativeMemory == null ? stackalloc byte[bufferSize] : new Span(nativeMemory, bufferSize); + + Encoding.UTF8.GetBytes(interfaceName, buffer); + buffer[bufferSize - 1] = 0; + + return Interop.Sys.InterfaceNameToIndex(buffer); + } + finally + { + if (nativeMemory != null) + { + NativeMemory.Free(nativeMemory); + } + } + } + + public static unsafe uint InterfaceNameToIndex(ReadOnlySpan interfaceName) + { + int bufferSize = interfaceName.Length + 1; + byte* nativeMemory = bufferSize <= StackAllocationThreshold ? null : (byte*)NativeMemory.Alloc((nuint)bufferSize); + + try + { + Span buffer = nativeMemory == null ? stackalloc byte[bufferSize] : new Span(nativeMemory, bufferSize); + + interfaceName.CopyTo(buffer); + buffer[bufferSize - 1] = 0; + + return Interop.Sys.InterfaceNameToIndex(buffer); + } + finally + { + if (nativeMemory != null) + { + NativeMemory.Free(nativeMemory); + } + } + } } } diff --git a/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Windows.cs b/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Windows.cs index 4912d17029d1f6..175934f57b14fa 100644 --- a/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Windows.cs +++ b/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Windows.cs @@ -1,13 +1,102 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Runtime.InteropServices; +using System.Text; + namespace System.Net.NetworkInformation { internal static class InterfaceInfoPal { + private const int StackAllocationThreshold = 512; + public static uint InterfaceNameToIndex(string interfaceName) { return Interop.IpHlpApi.if_nametoindex(interfaceName); } + + public static unsafe uint InterfaceNameToIndex(ReadOnlySpan interfaceName) + { + // Includes null terminator. + int bufferSize = GetAnsiStringByteCount(interfaceName); + byte* nativeMemory = bufferSize <= StackAllocationThreshold ? null : (byte*)NativeMemory.Alloc((nuint)bufferSize); + + try + { + Span buffer = nativeMemory == null ? stackalloc byte[bufferSize] : new Span(nativeMemory, bufferSize); + + GetAnsiStringBytes(interfaceName, buffer); + return Interop.IpHlpApi.if_nametoindex(buffer); + } + finally + { + if (nativeMemory != null) + { + NativeMemory.Free(nativeMemory); + } + } + } + + public static uint InterfaceNameToIndex(ReadOnlySpan interfaceName) + { + // The interface name passed to Windows' if_nametoindex function must be marshalled as an ANSI string. + // As a result, a UTF8 string requires two transcoding steps: UTF8 to Unicode, and Unicode to ANSI. + int bufferLength = Encoding.UTF8.GetCharCount(interfaceName); + Span buffer = bufferLength <= StackAllocationThreshold ? stackalloc char[bufferLength] : new char[bufferLength]; + + Encoding.UTF8.GetChars(interfaceName, buffer); + return InterfaceNameToIndex(buffer); + } + + // This method is replicated from Marshal.GetAnsiStringByteCount (which is internal to System.Private.CoreLib.) + private static unsafe int GetAnsiStringByteCount(ReadOnlySpan chars) + { + int byteLength; + + if (chars.Length == 0) + { + byteLength = 0; + } + else + { + fixed (char* pChars = chars) + { + byteLength = Interop.Kernel32.WideCharToMultiByte( + Interop.Kernel32.CP_ACP, Interop.Kernel32.WC_NO_BEST_FIT_CHARS, pChars, chars.Length, null, 0, null, null); + if (byteLength <= 0) + { + throw new ArgumentException(); + } + } + } + + return checked(byteLength + 1); + } + + // This method is replicated from Marshal.GetAnsiStringBytes (which is internal to System.Private.CoreLib.) + private static unsafe void GetAnsiStringBytes(ReadOnlySpan chars, Span bytes) + { + int byteLength; + + if (chars.Length == 0) + { + byteLength = 0; + } + else + { + fixed (char* pChars = chars) + fixed (byte* pBytes = bytes) + { + byteLength = Interop.Kernel32.WideCharToMultiByte( + Interop.Kernel32.CP_ACP, Interop.Kernel32.WC_NO_BEST_FIT_CHARS, pChars, chars.Length, pBytes, bytes.Length, null, null); + if (byteLength <= 0) + { + throw new ArgumentException(); + } + } + } + + bytes[byteLength] = 0; + } } } diff --git a/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj b/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj index e8a1519ca380d3..e2aa3a922d0869 100644 --- a/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj +++ b/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj @@ -110,6 +110,8 @@ Link="Common\Interop\Windows\IpHlpApi\Interop.FIXED_INFO.cs" /> + + diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs index 6ddde36e1fdeb3..c77aed2db334d6 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs @@ -89,7 +89,7 @@ private static unsafe bool TryParseIPv6(ReadOnlySpan ipSpan, Span< if (scopeIdSpan.Length > 1) { - string interfaceName = string.Empty; + uint interfaceIndex; scopeIdSpan = scopeIdSpan.Slice(1); // scopeId is a numeric value @@ -102,7 +102,7 @@ private static unsafe bool TryParseIPv6(ReadOnlySpan ipSpan, Span< return true; } - interfaceName = System.Text.Encoding.UTF8.GetString(castScopeIdSpan); + interfaceIndex = InterfaceInfoPal.InterfaceNameToIndex(castScopeIdSpan); } else if (typeof(TChar) == typeof(char)) { @@ -113,10 +113,13 @@ private static unsafe bool TryParseIPv6(ReadOnlySpan ipSpan, Span< return true; } - interfaceName = new string(castScopeIdSpan); + interfaceIndex = InterfaceInfoPal.InterfaceNameToIndex(castScopeIdSpan); + } + else + { + interfaceIndex = 0; } - uint interfaceIndex = InterfaceInfoPal.InterfaceNameToIndex(interfaceName); if (interfaceIndex > 0) { scope = interfaceIndex; diff --git a/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsing.cs b/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsing.cs index 8a992abd015b76..7b7cc44b3f3129 100644 --- a/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsing.cs +++ b/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsing.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Net.Sockets; +using System.Runtime.InteropServices; using System.Text; using Xunit; @@ -486,16 +487,29 @@ public void TryParseIPv6_ValidAddress_RoundtripMatchesExpected(string address, s } } - public static readonly object[][] ScopeIds = + public static IEnumerable ScopeIds() { - new object[] { "Fe08::1%123", 123}, - new object[] { "Fe08::1%12345678", 12345678}, - new object[] { "fe80::e8b0:63ff:fee8:6b3b%9", 9}, - new object[] { "fe80::e8b0:63ff:fee8:6b3b", 0}, - new object[] { "fe80::e8b0:63ff:fee8:6b3b%abcd0", 0}, - new object[] { "::%unknownInterface", 0}, - new object[] { "::%0", 0}, - }; + yield return new object[] { "Fe08::1%123", 123 }; + yield return new object[] { "Fe08::1%12345678", 12345678 }; + yield return new object[] { "fe80::e8b0:63ff:fee8:6b3b%9", 9 }; + yield return new object[] { "fe80::e8b0:63ff:fee8:6b3b", 0 }; + yield return new object[] { "fe80::e8b0:63ff:fee8:6b3b%abcd0", 0 }; + yield return new object[] { "::%unknownInterface", 0 }; + yield return new object[] { "::%0", 0 }; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + yield return new object[] { "::%loopback_0", System.Net.NetworkInformation.NetworkInterface.IPv6LoopbackInterfaceIndex }; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD)) + { + yield return new object[] { "::%lo0", System.Net.NetworkInformation.NetworkInterface.IPv6LoopbackInterfaceIndex }; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + yield return new object[] { "::%lo", System.Net.NetworkInformation.NetworkInterface.IPv6LoopbackInterfaceIndex }; + } + } [Theory] [MemberData(nameof(ScopeIds))] diff --git a/src/libraries/System.Net.Primitives/tests/PalTests/System.Net.Primitives.Pal.Tests.csproj b/src/libraries/System.Net.Primitives/tests/PalTests/System.Net.Primitives.Pal.Tests.csproj index 90fbc1e3d31bab..63540f35694a80 100644 --- a/src/libraries/System.Net.Primitives/tests/PalTests/System.Net.Primitives.Pal.Tests.csproj +++ b/src/libraries/System.Net.Primitives/tests/PalTests/System.Net.Primitives.Pal.Tests.csproj @@ -67,6 +67,8 @@ Link="Common\System\Net\SocketAddressPal.Windows.cs" /> + + + + Date: Mon, 22 Jul 2024 21:45:07 +0100 Subject: [PATCH 36/43] PInvoke/PAL cleanup There are now no circumstances where a string is passed to if_nametoindex or SystemNative_InterfaceNameToIndex, or the calling InterfaceNameToIndex method in InterfaceInfoPal. Removed these. --- .../Unix/System.Native/Interop.InterfaceNameToIndex.cs | 3 --- .../src/Interop/Windows/IpHlpApi/Interop.if_nametoindex.cs | 3 --- .../Net/NetworkInformation/InterfaceInfoPal.Browser.cs | 6 ------ .../System/Net/NetworkInformation/InterfaceInfoPal.Unix.cs | 5 ----- .../Net/NetworkInformation/InterfaceInfoPal.Windows.cs | 5 ----- 5 files changed, 22 deletions(-) diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.InterfaceNameToIndex.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.InterfaceNameToIndex.cs index 1d7986480b5d8f..989219241bf30a 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.InterfaceNameToIndex.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.InterfaceNameToIndex.cs @@ -8,9 +8,6 @@ internal static partial class Interop { internal static partial class Sys { - [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_InterfaceNameToIndex", StringMarshalling = StringMarshalling.Utf8, SetLastError = true)] - public static partial uint InterfaceNameToIndex(string name); - [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_InterfaceNameToIndex", SetLastError = true)] public static partial uint InterfaceNameToIndex(ReadOnlySpan utf8NullTerminatedName); } diff --git a/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.if_nametoindex.cs b/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.if_nametoindex.cs index ee311a26584204..007bea17ad7dec 100644 --- a/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.if_nametoindex.cs +++ b/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.if_nametoindex.cs @@ -8,9 +8,6 @@ internal static partial class Interop { internal static partial class IpHlpApi { - [LibraryImport(Interop.Libraries.IpHlpApi, SetLastError = true)] - internal static partial uint if_nametoindex([MarshalAs(UnmanagedType.LPStr)] string name); - [LibraryImport(Interop.Libraries.IpHlpApi, SetLastError = true)] internal static partial uint if_nametoindex(ReadOnlySpan ansiNullTerminatedName); } diff --git a/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Browser.cs b/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Browser.cs index 338930b3cfccf7..3516e923f020a2 100644 --- a/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Browser.cs +++ b/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Browser.cs @@ -5,12 +5,6 @@ namespace System.Net.NetworkInformation { internal static class InterfaceInfoPal { - public static uint InterfaceNameToIndex(string _/*interfaceName*/) - { - // zero means "unknown" - return 0; - } - public static uint InterfaceNameToIndex(ReadOnlySpan _/*interfaceName*/) { // zero means "unknown" diff --git a/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Unix.cs b/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Unix.cs index 5ded8532c33c60..0d31715a93d841 100644 --- a/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Unix.cs +++ b/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Unix.cs @@ -10,11 +10,6 @@ internal static class InterfaceInfoPal { private const int StackAllocationThreshold = 512; - public static uint InterfaceNameToIndex(string interfaceName) - { - return Interop.Sys.InterfaceNameToIndex(interfaceName); - } - public static unsafe uint InterfaceNameToIndex(ReadOnlySpan interfaceName) { // Includes null terminator. diff --git a/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Windows.cs b/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Windows.cs index 175934f57b14fa..a52f643a8b0bea 100644 --- a/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Windows.cs +++ b/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Windows.cs @@ -10,11 +10,6 @@ internal static class InterfaceInfoPal { private const int StackAllocationThreshold = 512; - public static uint InterfaceNameToIndex(string interfaceName) - { - return Interop.IpHlpApi.if_nametoindex(interfaceName); - } - public static unsafe uint InterfaceNameToIndex(ReadOnlySpan interfaceName) { // Includes null terminator. From c352e018a0a9b92ad83984bfc7e7c8c9f9c56453 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Mon, 22 Jul 2024 23:43:57 +0100 Subject: [PATCH 37/43] Added assertion An interface name comes from a scope in a valid IPv6 address. There'll always be at least three characters (::%) in front of the scope, so taking the length of an interface name which stripping these three leading characters away and adds a single null terminator will never overflow an integer. --- .../NetworkInformation/InterfaceInfoPal.Unix.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Unix.cs b/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Unix.cs index 0d31715a93d841..cd632dc43e9e9f 100644 --- a/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Unix.cs +++ b/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Unix.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; @@ -12,8 +13,11 @@ internal static class InterfaceInfoPal public static unsafe uint InterfaceNameToIndex(ReadOnlySpan interfaceName) { + int byteCount = Encoding.UTF8.GetByteCount(interfaceName); + + Debug.Assert(byteCount <= int.MaxValue - 1); // Includes null terminator. - int bufferSize = Encoding.UTF8.GetByteCount(interfaceName) + 1; + int bufferSize = byteCount + 1; byte* nativeMemory = bufferSize <= StackAllocationThreshold ? null : (byte*)NativeMemory.Alloc((nuint)bufferSize); try @@ -21,7 +25,7 @@ public static unsafe uint InterfaceNameToIndex(ReadOnlySpan interfaceName) Span buffer = nativeMemory == null ? stackalloc byte[bufferSize] : new Span(nativeMemory, bufferSize); Encoding.UTF8.GetBytes(interfaceName, buffer); - buffer[bufferSize - 1] = 0; + buffer[byteCount] = 0; return Interop.Sys.InterfaceNameToIndex(buffer); } @@ -36,7 +40,11 @@ public static unsafe uint InterfaceNameToIndex(ReadOnlySpan interfaceName) public static unsafe uint InterfaceNameToIndex(ReadOnlySpan interfaceName) { - int bufferSize = interfaceName.Length + 1; + int byteCount = interfaceName.Length; + + Debug.Assert(byteCount <= int.MaxValue - 1); + // Includes null terminator. + int bufferSize = byteCount + 1; byte* nativeMemory = bufferSize <= StackAllocationThreshold ? null : (byte*)NativeMemory.Alloc((nuint)bufferSize); try @@ -44,7 +52,7 @@ public static unsafe uint InterfaceNameToIndex(ReadOnlySpan interfaceName) Span buffer = nativeMemory == null ? stackalloc byte[bufferSize] : new Span(nativeMemory, bufferSize); interfaceName.CopyTo(buffer); - buffer[bufferSize - 1] = 0; + buffer[byteCount] = 0; return Interop.Sys.InterfaceNameToIndex(buffer); } From 2da63cb2b5067f73f4796481d9736299a029cea7 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sun, 28 Jul 2024 19:30:22 +0100 Subject: [PATCH 38/43] Replaced usage of if_nametoindex This eliminates a number of changes required to transcode from UTF8 and Unicode to ANSI. Instead of using if_nametoindex on Windows, this now uses ConvertInterfaceNameToLuidW & ConvertInterfaceLuidToIndex. --- ...=> Interop.ConvertInterfaceLuidToIndex.cs} | 4 +- .../Interop.ConvertInterfaceNameToLuid.cs | 14 +++ .../InterfaceInfoPal.Browser.cs | 11 +- .../InterfaceInfoPal.Unix.cs | 67 ++++++----- .../InterfaceInfoPal.Windows.cs | 112 ++++++++---------- .../src/System.Net.Primitives.csproj | 10 +- .../src/System/Net/IPAddressParser.cs | 31 ++--- .../System.Net.Primitives.Pal.Tests.csproj | 10 +- ...stem.Net.Primitives.UnitTests.Tests.csproj | 10 +- 9 files changed, 129 insertions(+), 140 deletions(-) rename src/libraries/Common/src/Interop/Windows/IpHlpApi/{Interop.if_nametoindex.cs => Interop.ConvertInterfaceLuidToIndex.cs} (62%) create mode 100644 src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceNameToLuid.cs diff --git a/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.if_nametoindex.cs b/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceLuidToIndex.cs similarity index 62% rename from src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.if_nametoindex.cs rename to src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceLuidToIndex.cs index 007bea17ad7dec..95a9a0fa2c12d6 100644 --- a/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.if_nametoindex.cs +++ b/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceLuidToIndex.cs @@ -8,7 +8,7 @@ internal static partial class Interop { internal static partial class IpHlpApi { - [LibraryImport(Interop.Libraries.IpHlpApi, SetLastError = true)] - internal static partial uint if_nametoindex(ReadOnlySpan ansiNullTerminatedName); + [LibraryImport(Libraries.IpHlpApi, SetLastError = true)] + internal static partial uint ConvertInterfaceLuidToIndex(in ulong interfaceLuid, ref uint ifIndex); } } diff --git a/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceNameToLuid.cs b/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceNameToLuid.cs new file mode 100644 index 00000000000000..ea956d7de8ff6b --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceNameToLuid.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class IpHlpApi + { + [LibraryImport(Libraries.IpHlpApi, SetLastError = true, StringMarshalling = StringMarshalling.Utf16, EntryPoint = "ConvertInterfaceNameToLuidW")] + internal static unsafe partial uint ConvertInterfaceNameToLuid(ReadOnlySpan unicodeNullTerminatedName, ref ulong interfaceLuid); + } +} diff --git a/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Browser.cs b/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Browser.cs index 3516e923f020a2..26e47cdcf5992d 100644 --- a/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Browser.cs +++ b/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Browser.cs @@ -1,17 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Numerics; + namespace System.Net.NetworkInformation { internal static class InterfaceInfoPal { - public static uint InterfaceNameToIndex(ReadOnlySpan _/*interfaceName*/) - { - // zero means "unknown" - return 0; - } - - public static uint InterfaceNameToIndex(ReadOnlySpan _/*interfaceName*/) + public static uint InterfaceNameToIndex(ReadOnlySpan _/*interfaceName*/) + where TChar : unmanaged, IBinaryNumber { // zero means "unknown" return 0; diff --git a/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Unix.cs b/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Unix.cs index cd632dc43e9e9f..f668f3a1be1141 100644 --- a/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Unix.cs +++ b/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Unix.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Numerics; using System.Runtime.InteropServices; using System.Text; @@ -9,50 +10,54 @@ namespace System.Net.NetworkInformation { internal static class InterfaceInfoPal { + // Measured in bytes (the native data type for this platform's API.) + // Selected to occupy as much stack space as Windows' threshold (which is 256 two-byte characters.) private const int StackAllocationThreshold = 512; - public static unsafe uint InterfaceNameToIndex(ReadOnlySpan interfaceName) + public static unsafe uint InterfaceNameToIndex(ReadOnlySpan interfaceName) + where TChar : unmanaged, IBinaryNumber { - int byteCount = Encoding.UTF8.GetByteCount(interfaceName); + Debug.Assert(typeof(TChar) == typeof(byte) || typeof(TChar) == typeof(char)); - Debug.Assert(byteCount <= int.MaxValue - 1); - // Includes null terminator. - int bufferSize = byteCount + 1; - byte* nativeMemory = bufferSize <= StackAllocationThreshold ? null : (byte*)NativeMemory.Alloc((nuint)bufferSize); + // Measured in bytes, including null terminator. + int bufferSize = 0; + byte* nativeMemory = null; - try + // The underlying API for this method accepts a null-terminated UTF8 string containing the interface name. + // If TChar is byte, the only work required is a byte copy. If TChar is char, there's a transcoding step from Unicode + // to UTF8. + // Notably: the encoding passed to the underlying API is different between Linux and Windows. + if (typeof(TChar) == typeof(byte)) { - Span buffer = nativeMemory == null ? stackalloc byte[bufferSize] : new Span(nativeMemory, bufferSize); - - Encoding.UTF8.GetBytes(interfaceName, buffer); - buffer[byteCount] = 0; - - return Interop.Sys.InterfaceNameToIndex(buffer); + bufferSize = interfaceName.Length + 1; } - finally + else if (typeof(TChar) == typeof(char)) { - if (nativeMemory != null) - { - NativeMemory.Free(nativeMemory); - } - } - } + ReadOnlySpan castInterfaceName = MemoryMarshal.Cast(interfaceName); - public static unsafe uint InterfaceNameToIndex(ReadOnlySpan interfaceName) - { - int byteCount = interfaceName.Length; - - Debug.Assert(byteCount <= int.MaxValue - 1); - // Includes null terminator. - int bufferSize = byteCount + 1; - byte* nativeMemory = bufferSize <= StackAllocationThreshold ? null : (byte*)NativeMemory.Alloc((nuint)bufferSize); + bufferSize = Encoding.UTF8.GetByteCount(castInterfaceName) + 1; + } + Debug.Assert(bufferSize <= int.MaxValue - 1); try { - Span buffer = nativeMemory == null ? stackalloc byte[bufferSize] : new Span(nativeMemory, bufferSize); + nativeMemory = bufferSize <= StackAllocationThreshold ? null : (byte*)NativeMemory.Alloc((nuint)bufferSize); + + Span buffer = nativeMemory == null ? stackalloc byte[StackAllocationThreshold].Slice(0, bufferSize) : new Span(nativeMemory, bufferSize); + + if (typeof(TChar) == typeof(byte)) + { + ReadOnlySpan castInterfaceName = MemoryMarshal.Cast(interfaceName); - interfaceName.CopyTo(buffer); - buffer[byteCount] = 0; + castInterfaceName.CopyTo(buffer); + } + else if (typeof(TChar) == typeof(char)) + { + ReadOnlySpan castInterfaceName = MemoryMarshal.Cast(interfaceName); + + Encoding.UTF8.GetBytes(castInterfaceName, buffer); + } + buffer[buffer.Length - 1] = 0; return Interop.Sys.InterfaceNameToIndex(buffer); } diff --git a/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Windows.cs b/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Windows.cs index a52f643a8b0bea..18fd70c8e02402 100644 --- a/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Windows.cs +++ b/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Windows.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; +using System.Numerics; using System.Runtime.InteropServices; using System.Text; @@ -8,90 +10,72 @@ namespace System.Net.NetworkInformation { internal static class InterfaceInfoPal { - private const int StackAllocationThreshold = 512; + // Measured in characters (the native data type for this platform's API.) + private const int StackAllocationThreshold = 256; - public static unsafe uint InterfaceNameToIndex(ReadOnlySpan interfaceName) + public static unsafe uint InterfaceNameToIndex(ReadOnlySpan interfaceName) + where TChar : unmanaged, IBinaryNumber { - // Includes null terminator. - int bufferSize = GetAnsiStringByteCount(interfaceName); - byte* nativeMemory = bufferSize <= StackAllocationThreshold ? null : (byte*)NativeMemory.Alloc((nuint)bufferSize); + Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); - try - { - Span buffer = nativeMemory == null ? stackalloc byte[bufferSize] : new Span(nativeMemory, bufferSize); + // Measured in characters, including null terminator. + int bufferSize = 0; + char* nativeMemory = null; + ulong interfaceLuid = 0; + uint interfaceIndex = 0; - GetAnsiStringBytes(interfaceName, buffer); - return Interop.IpHlpApi.if_nametoindex(buffer); - } - finally + // The underlying API for this method accepts a null-terminated Unicode string containing the interface name. + // If TChar is char, the only work required is a byte copy. If TChar is byte, there's a transcoding step from UTF8 + // to Unicode. + // Notably: the encoding passed to the underlying API is different between Linux and Windows. + if (typeof(TChar) == typeof(char)) { - if (nativeMemory != null) - { - NativeMemory.Free(nativeMemory); - } + bufferSize = interfaceName.Length + 1; } - } + else if (typeof(TChar) == typeof(byte)) + { + ReadOnlySpan castInterfaceName = MemoryMarshal.Cast(interfaceName); - public static uint InterfaceNameToIndex(ReadOnlySpan interfaceName) - { - // The interface name passed to Windows' if_nametoindex function must be marshalled as an ANSI string. - // As a result, a UTF8 string requires two transcoding steps: UTF8 to Unicode, and Unicode to ANSI. - int bufferLength = Encoding.UTF8.GetCharCount(interfaceName); - Span buffer = bufferLength <= StackAllocationThreshold ? stackalloc char[bufferLength] : new char[bufferLength]; + bufferSize = Encoding.UTF8.GetCharCount(castInterfaceName) + 1; + } + Debug.Assert(bufferSize <= int.MaxValue - 1); - Encoding.UTF8.GetChars(interfaceName, buffer); - return InterfaceNameToIndex(buffer); - } + try + { + nativeMemory = bufferSize <= StackAllocationThreshold ? null : (char*)NativeMemory.Alloc((nuint)(bufferSize * sizeof(char))); - // This method is replicated from Marshal.GetAnsiStringByteCount (which is internal to System.Private.CoreLib.) - private static unsafe int GetAnsiStringByteCount(ReadOnlySpan chars) - { - int byteLength; + Span buffer = nativeMemory == null ? stackalloc char[StackAllocationThreshold].Slice(0, bufferSize) : new Span(nativeMemory, bufferSize); - if (chars.Length == 0) - { - byteLength = 0; - } - else - { - fixed (char* pChars = chars) + if (typeof(TChar) == typeof(char)) { - byteLength = Interop.Kernel32.WideCharToMultiByte( - Interop.Kernel32.CP_ACP, Interop.Kernel32.WC_NO_BEST_FIT_CHARS, pChars, chars.Length, null, 0, null, null); - if (byteLength <= 0) - { - throw new ArgumentException(); - } - } - } + ReadOnlySpan castInterfaceName = MemoryMarshal.Cast(interfaceName); - return checked(byteLength + 1); - } + castInterfaceName.CopyTo(buffer); + } + else if (typeof(TChar) == typeof(byte)) + { + ReadOnlySpan castInterfaceName = MemoryMarshal.Cast(interfaceName); - // This method is replicated from Marshal.GetAnsiStringBytes (which is internal to System.Private.CoreLib.) - private static unsafe void GetAnsiStringBytes(ReadOnlySpan chars, Span bytes) - { - int byteLength; + Encoding.UTF8.GetChars(castInterfaceName, buffer); + } + buffer[buffer.Length - 1] = '\0'; - if (chars.Length == 0) - { - byteLength = 0; + if (Interop.IpHlpApi.ConvertInterfaceNameToLuid(buffer, ref interfaceLuid) != 0) + { + return 0; + } } - else + finally { - fixed (char* pChars = chars) - fixed (byte* pBytes = bytes) + if (nativeMemory != null) { - byteLength = Interop.Kernel32.WideCharToMultiByte( - Interop.Kernel32.CP_ACP, Interop.Kernel32.WC_NO_BEST_FIT_CHARS, pChars, chars.Length, pBytes, bytes.Length, null, null); - if (byteLength <= 0) - { - throw new ArgumentException(); - } + NativeMemory.Free(nativeMemory); } } - bytes[byteLength] = 0; + return Interop.IpHlpApi.ConvertInterfaceLuidToIndex(interfaceLuid, ref interfaceIndex) == 0 + ? interfaceIndex + : 0; } } } diff --git a/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj b/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj index e2aa3a922d0869..78b92c9adb6008 100644 --- a/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj +++ b/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj @@ -110,8 +110,6 @@ Link="Common\Interop\Windows\IpHlpApi\Interop.FIXED_INFO.cs" /> - - - + + diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs index c77aed2db334d6..7ef1cd216e99cb 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs @@ -89,7 +89,7 @@ private static unsafe bool TryParseIPv6(ReadOnlySpan ipSpan, Span< if (scopeIdSpan.Length > 1) { - uint interfaceIndex; + bool parsedNumericScope = false; scopeIdSpan = scopeIdSpan.Slice(1); // scopeId is a numeric value @@ -97,33 +97,28 @@ private static unsafe bool TryParseIPv6(ReadOnlySpan ipSpan, Span< { ReadOnlySpan castScopeIdSpan = MemoryMarshal.Cast(scopeIdSpan); - if (uint.TryParse(castScopeIdSpan, NumberStyles.None, CultureInfo.InvariantCulture, out scope)) - { - return true; - } - - interfaceIndex = InterfaceInfoPal.InterfaceNameToIndex(castScopeIdSpan); + parsedNumericScope = uint.TryParse(castScopeIdSpan, NumberStyles.None, CultureInfo.InvariantCulture, out scope); } else if (typeof(TChar) == typeof(char)) { ReadOnlySpan castScopeIdSpan = MemoryMarshal.Cast(scopeIdSpan); - if (uint.TryParse(castScopeIdSpan, NumberStyles.None, CultureInfo.InvariantCulture, out scope)) - { - return true; - } + parsedNumericScope = uint.TryParse(castScopeIdSpan, NumberStyles.None, CultureInfo.InvariantCulture, out scope); + } - interfaceIndex = InterfaceInfoPal.InterfaceNameToIndex(castScopeIdSpan); + if (parsedNumericScope) + { + return true; } else { - interfaceIndex = 0; - } + uint interfaceIndex = InterfaceInfoPal.InterfaceNameToIndex(scopeIdSpan); - if (interfaceIndex > 0) - { - scope = interfaceIndex; - return true; // scopeId is a known interface name + if (interfaceIndex > 0) + { + scope = interfaceIndex; + return true; // scopeId is a known interface name + } } // scopeId is an unknown interface name diff --git a/src/libraries/System.Net.Primitives/tests/PalTests/System.Net.Primitives.Pal.Tests.csproj b/src/libraries/System.Net.Primitives/tests/PalTests/System.Net.Primitives.Pal.Tests.csproj index 63540f35694a80..d70b43b977a4bb 100644 --- a/src/libraries/System.Net.Primitives/tests/PalTests/System.Net.Primitives.Pal.Tests.csproj +++ b/src/libraries/System.Net.Primitives/tests/PalTests/System.Net.Primitives.Pal.Tests.csproj @@ -67,8 +67,6 @@ Link="Common\System\Net\SocketAddressPal.Windows.cs" /> - - - + + - - - + + From 1ba71075dec920433024d033e2972092fee70af3 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Tue, 30 Jul 2024 19:39:18 +0100 Subject: [PATCH 39/43] Moved interface name tests When running in a container, Windows does not have a static loopback interface name. Moved tests of InterfaceInfoPal to the PalTests project, rather than testing with hardcoded IP addresses with real scope IDs. Made the tests use real (and in Windows' case, dynamic) interface names. --- .../Interop.ConvertInterfaceIndexToLuid.cs | 14 ++++ .../Interop.ConvertInterfaceLuidToName.cs | 14 ++++ .../tests/FunctionalTests/IPAddressParsing.cs | 31 ++----- .../PalTests/InterfaceInfoPalTest.Unix.cs | 37 +++++++++ .../PalTests/InterfaceInfoPalTest.Windows.cs | 83 +++++++++++++++++++ .../System.Net.Primitives.Pal.Tests.csproj | 6 ++ 6 files changed, 163 insertions(+), 22 deletions(-) create mode 100644 src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceIndexToLuid.cs create mode 100644 src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceLuidToName.cs create mode 100644 src/libraries/System.Net.Primitives/tests/PalTests/InterfaceInfoPalTest.Unix.cs create mode 100644 src/libraries/System.Net.Primitives/tests/PalTests/InterfaceInfoPalTest.Windows.cs diff --git a/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceIndexToLuid.cs b/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceIndexToLuid.cs new file mode 100644 index 00000000000000..8cd6103f3c5870 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceIndexToLuid.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class IpHlpApi + { + [LibraryImport(Libraries.IpHlpApi, SetLastError = true)] + internal static unsafe partial uint ConvertInterfaceIndexToLuid(uint ifIndex, ref ulong interfaceLuid); + } +} diff --git a/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceLuidToName.cs b/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceLuidToName.cs new file mode 100644 index 00000000000000..d1ce05d977bd5d --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceLuidToName.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class IpHlpApi + { + [LibraryImport(Libraries.IpHlpApi, SetLastError = true, StringMarshalling = StringMarshalling.Utf16, EntryPoint = "ConvertInterfaceLuidToNameW")] + internal static unsafe partial uint ConvertInterfaceLuidToName(in ulong interfaceLuid, Span name, int nameLength); + } +} diff --git a/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsing.cs b/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsing.cs index 7b7cc44b3f3129..9331bf6b3b98c0 100644 --- a/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsing.cs +++ b/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsing.cs @@ -487,29 +487,16 @@ public void TryParseIPv6_ValidAddress_RoundtripMatchesExpected(string address, s } } - public static IEnumerable ScopeIds() + public static readonly object[][] ScopeIds = { - yield return new object[] { "Fe08::1%123", 123 }; - yield return new object[] { "Fe08::1%12345678", 12345678 }; - yield return new object[] { "fe80::e8b0:63ff:fee8:6b3b%9", 9 }; - yield return new object[] { "fe80::e8b0:63ff:fee8:6b3b", 0 }; - yield return new object[] { "fe80::e8b0:63ff:fee8:6b3b%abcd0", 0 }; - yield return new object[] { "::%unknownInterface", 0 }; - yield return new object[] { "::%0", 0 }; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - yield return new object[] { "::%loopback_0", System.Net.NetworkInformation.NetworkInterface.IPv6LoopbackInterfaceIndex }; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD)) - { - yield return new object[] { "::%lo0", System.Net.NetworkInformation.NetworkInterface.IPv6LoopbackInterfaceIndex }; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - yield return new object[] { "::%lo", System.Net.NetworkInformation.NetworkInterface.IPv6LoopbackInterfaceIndex }; - } - } + new object[] { "Fe08::1%123", 123 }, + new object[] { "Fe08::1%12345678", 12345678 }, + new object[] { "fe80::e8b0:63ff:fee8:6b3b%9", 9 }, + new object[] { "fe80::e8b0:63ff:fee8:6b3b", 0 }, + new object[] { "fe80::e8b0:63ff:fee8:6b3b%abcd0", 0 }, + new object[] { "::%unknownInterface", 0 }, + new object[] { "::%0", 0 }, + }; [Theory] [MemberData(nameof(ScopeIds))] diff --git a/src/libraries/System.Net.Primitives/tests/PalTests/InterfaceInfoPalTest.Unix.cs b/src/libraries/System.Net.Primitives/tests/PalTests/InterfaceInfoPalTest.Unix.cs new file mode 100644 index 00000000000000..32e54206338017 --- /dev/null +++ b/src/libraries/System.Net.Primitives/tests/PalTests/InterfaceInfoPalTest.Unix.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Net.NetworkInformation; +using System.Runtime.InteropServices; +using System.Text; +using Xunit; + +namespace System.Net.Primitives.PalTests +{ + public class InterfaceInfoPalTests + { + public static IEnumerable InterfaceNames() + { + // By default, Linux will have a network interface named "lo" or "lo0". + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD)) + { + yield return new object[] { "lo0", NetworkInterface.IPv6LoopbackInterfaceIndex }; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + yield return new object[] { "lo", NetworkInterface.IPv6LoopbackInterfaceIndex }; + } + } + + [Theory] + [MemberData(nameof(InterfaceNames))] + public void Interface_Name_Round_Trips_To_Index(string ifName, uint ifIdx) + { + byte[] utf8Bytes = Encoding.UTF8.GetBytes(ifName); + + Assert.Equal(ifIdx, InterfaceInfoPal.InterfaceNameToIndex(ifName.AsSpan())); + Assert.Equal(ifIdx, InterfaceInfoPal.InterfaceNameToIndex(utf8Bytes)); + } + } +} diff --git a/src/libraries/System.Net.Primitives/tests/PalTests/InterfaceInfoPalTest.Windows.cs b/src/libraries/System.Net.Primitives/tests/PalTests/InterfaceInfoPalTest.Windows.cs new file mode 100644 index 00000000000000..92c72f7e152fad --- /dev/null +++ b/src/libraries/System.Net.Primitives/tests/PalTests/InterfaceInfoPalTest.Windows.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Net.NetworkInformation; +using System.Text; +using Xunit; + +namespace System.Net.Primitives.PalTests +{ + public class InterfaceInfoPalTests + { + private static string GetInterfaceName(uint ifIdx) + { + ulong loopbackLuid = 0; + + if (Interop.IpHlpApi.ConvertInterfaceIndexToLuid(ifIdx, ref loopbackLuid) == 0) + { + Span ifNameBuffer = stackalloc char[256]; + + if (Interop.IpHlpApi.ConvertInterfaceLuidToName(loopbackLuid, ifNameBuffer, ifNameBuffer.Length) == 0) + { + int nullTerminatorIdx = ifNameBuffer.IndexOf('\0'); + + if (nullTerminatorIdx != -1) + { + Span ifName = ifNameBuffer.Slice(0, nullTerminatorIdx); + + return ifName.ToString(); + } + } + } + + return null; + } + + public static IEnumerable InterfaceNames() + { + // Windows will usually name its loopback interface "loopback_0", except when it runs in a container. + string loopbackInterface = GetInterfaceName((uint)NetworkInterface.IPv6LoopbackInterfaceIndex); + + if (loopbackInterface != null) + { + yield return new object[] { loopbackInterface, NetworkInterface.IPv6LoopbackInterfaceIndex }; + } + + foreach (NetworkInterface ni in NetworkInterface.GetAllNetworkInterfaces()) + { + uint ifIdx; + string ifName; + + if (ni.Supports(NetworkInterfaceComponent.IPv6)) + { + ifIdx = (uint)ni.GetIPProperties().GetIPv6Properties().Index; + } + else if (ni.Supports(NetworkInterfaceComponent.IPv4)) + { + ifIdx = (uint)ni.GetIPProperties().GetIPv4Properties().Index; + } + else + { + continue; + } + + ifName = GetInterfaceName(ifIdx); + if (ifName != null) + { + yield return new object[] { ifName, ifIdx }; + } + } + } + + [Theory] + [MemberData(nameof(InterfaceNames))] + public void Interface_Name_Round_Trips_To_Index(string ifName, uint ifIdx) + { + byte[] utf8Bytes = Encoding.UTF8.GetBytes(ifName); + + Assert.Equal(ifIdx, InterfaceInfoPal.InterfaceNameToIndex(ifName.AsSpan())); + Assert.Equal(ifIdx, InterfaceInfoPal.InterfaceNameToIndex(utf8Bytes)); + } + } +} diff --git a/src/libraries/System.Net.Primitives/tests/PalTests/System.Net.Primitives.Pal.Tests.csproj b/src/libraries/System.Net.Primitives/tests/PalTests/System.Net.Primitives.Pal.Tests.csproj index d70b43b977a4bb..111ddf6a8af2d2 100644 --- a/src/libraries/System.Net.Primitives/tests/PalTests/System.Net.Primitives.Pal.Tests.csproj +++ b/src/libraries/System.Net.Primitives/tests/PalTests/System.Net.Primitives.Pal.Tests.csproj @@ -61,6 +61,7 @@ Link="ProductionCode\Common\Interop\Windows\WinSock\Interop.ErrorCodes.cs" /> + + + + Date: Sat, 5 Oct 2024 22:48:33 +0100 Subject: [PATCH 40/43] Corrected PInvoke signatures and Unix InterfaceInfoPal * SetLastError has the correct value in the Windows interop layer. * Preserve InterfaceNameToIndex's errno in the Unix InterfaceInfoPal. --- .../IpHlpApi/Interop.ConvertInterfaceIndexToLuid.cs | 2 +- .../IpHlpApi/Interop.ConvertInterfaceLuidToIndex.cs | 2 +- .../IpHlpApi/Interop.ConvertInterfaceLuidToName.cs | 2 +- .../IpHlpApi/Interop.ConvertInterfaceNameToLuid.cs | 11 +++++++++-- .../Common/src/System/Net/IPv6AddressHelper.Common.cs | 1 + .../Net/NetworkInformation/InterfaceInfoPal.Unix.cs | 3 +++ 6 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceIndexToLuid.cs b/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceIndexToLuid.cs index 8cd6103f3c5870..0bd1ab6aaa1c6c 100644 --- a/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceIndexToLuid.cs +++ b/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceIndexToLuid.cs @@ -8,7 +8,7 @@ internal static partial class Interop { internal static partial class IpHlpApi { - [LibraryImport(Libraries.IpHlpApi, SetLastError = true)] + [LibraryImport(Libraries.IpHlpApi)] internal static unsafe partial uint ConvertInterfaceIndexToLuid(uint ifIndex, ref ulong interfaceLuid); } } diff --git a/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceLuidToIndex.cs b/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceLuidToIndex.cs index 95a9a0fa2c12d6..c7dae9ccb30b70 100644 --- a/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceLuidToIndex.cs +++ b/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceLuidToIndex.cs @@ -8,7 +8,7 @@ internal static partial class Interop { internal static partial class IpHlpApi { - [LibraryImport(Libraries.IpHlpApi, SetLastError = true)] + [LibraryImport(Libraries.IpHlpApi)] internal static partial uint ConvertInterfaceLuidToIndex(in ulong interfaceLuid, ref uint ifIndex); } } diff --git a/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceLuidToName.cs b/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceLuidToName.cs index d1ce05d977bd5d..0e92a0c9d32cf8 100644 --- a/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceLuidToName.cs +++ b/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceLuidToName.cs @@ -8,7 +8,7 @@ internal static partial class Interop { internal static partial class IpHlpApi { - [LibraryImport(Libraries.IpHlpApi, SetLastError = true, StringMarshalling = StringMarshalling.Utf16, EntryPoint = "ConvertInterfaceLuidToNameW")] + [LibraryImport(Libraries.IpHlpApi, StringMarshalling = StringMarshalling.Utf16, EntryPoint = "ConvertInterfaceLuidToNameW")] internal static unsafe partial uint ConvertInterfaceLuidToName(in ulong interfaceLuid, Span name, int nameLength); } } diff --git a/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceNameToLuid.cs b/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceNameToLuid.cs index ea956d7de8ff6b..cd47fd379a519f 100644 --- a/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceNameToLuid.cs +++ b/src/libraries/Common/src/Interop/Windows/IpHlpApi/Interop.ConvertInterfaceNameToLuid.cs @@ -8,7 +8,14 @@ internal static partial class Interop { internal static partial class IpHlpApi { - [LibraryImport(Libraries.IpHlpApi, SetLastError = true, StringMarshalling = StringMarshalling.Utf16, EntryPoint = "ConvertInterfaceNameToLuidW")] - internal static unsafe partial uint ConvertInterfaceNameToLuid(ReadOnlySpan unicodeNullTerminatedName, ref ulong interfaceLuid); + /// + /// Converts a Unicode network interface name to the locally unique identifier (LUID) for the interface. + /// + /// + /// The NULL-terminated Unicode string containing the network interface name. + /// A pointer to the NET_LUID for this interface. + /// + [LibraryImport(Libraries.IpHlpApi, StringMarshalling = StringMarshalling.Utf16, EntryPoint = "ConvertInterfaceNameToLuidW")] + internal static unsafe partial uint ConvertInterfaceNameToLuid(ReadOnlySpan interfaceName, ref ulong interfaceLuid); } } diff --git a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs index 2d75b24258acc7..5c871c68a1a98a 100644 --- a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs @@ -321,6 +321,7 @@ internal static void Parse(ReadOnlySpan address, scoped Span.Empty; + // Skip the start '[' character, if present. Stop parsing at the end IPv6 address terminator (']'). for (int i = (address[0] == TChar.CreateTruncating('[') ? 1 : 0); i < address.Length && address[i] != TChar.CreateTruncating(']');) { diff --git a/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Unix.cs b/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Unix.cs index f668f3a1be1141..11f9fdb3fdc5d4 100644 --- a/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Unix.cs +++ b/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Unix.cs @@ -65,7 +65,10 @@ public static unsafe uint InterfaceNameToIndex(ReadOnlySpan interf { if (nativeMemory != null) { + int errNo = Marshal.GetLastSystemError(); + NativeMemory.Free(nativeMemory); + Marshal.SetLastPInvokeError(errNo); } } } From b79a6f58778d40823440d9d3ca6337c1e4afb37b Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sun, 6 Oct 2024 19:34:02 +0100 Subject: [PATCH 41/43] Continued work with code review feedback & benchmark --- .../System/Net/IPv4AddressHelper.Common.cs | 26 +++++++++++++++++-- .../InterfaceInfoPal.Unix.cs | 2 +- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs index 2f4e1dd50d813b..c5200796103db7 100644 --- a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs @@ -249,10 +249,26 @@ internal static unsafe long ParseNonCanonical(TChar* name, int start, ref for (; current < end; current++) { ch = name[current]; + int digitValue; int characterValue = int.CreateTruncating(ch); - int digitValue = HexConverter.FromChar(characterValue); - if (digitValue >= numberBase) + if ((numberBase == IPv4AddressHelper.Decimal || numberBase == IPv4AddressHelper.Hex) && '0' <= characterValue && characterValue <= '9') + { + digitValue = characterValue - '0'; + } + else if (numberBase == IPv4AddressHelper.Octal && '0' <= characterValue && characterValue <= '7') + { + digitValue = characterValue - '0'; + } + else if (numberBase == IPv4AddressHelper.Hex && 'a' <= characterValue && characterValue <= 'f') + { + digitValue = characterValue + 10 - 'a'; + } + else if (numberBase == IPv4AddressHelper.Hex && 'A' <= characterValue && characterValue <= 'F') + { + digitValue = characterValue + 10 - 'A'; + } + else { break; // Invalid/terminator } @@ -315,18 +331,24 @@ internal static unsafe long ParseNonCanonical(TChar* name, int start, ref case 0: // 0xFFFFFFFF return parts[0]; case 1: // 0xFF.0xFFFFFF + Debug.Assert(parts[0] <= 0xFF); if (parts[1] > 0xffffff) { return Invalid; } return (parts[0] << 24) | parts[1]; case 2: // 0xFF.0xFF.0xFFFF + Debug.Assert(parts[0] <= 0xFF); + Debug.Assert(parts[1] <= 0xFF); if (parts[2] > 0xffff) { return Invalid; } return (parts[0] << 24) | (parts[1] << 16) | parts[2]; case 3: // 0xFF.0xFF.0xFF.0xFF + Debug.Assert(parts[0] <= 0xFF); + Debug.Assert(parts[1] <= 0xFF); + Debug.Assert(parts[2] <= 0xFF); if (parts[3] > 0xff) { return Invalid; diff --git a/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Unix.cs b/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Unix.cs index 11f9fdb3fdc5d4..b9506deec474ad 100644 --- a/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Unix.cs +++ b/src/libraries/Common/src/System/Net/NetworkInformation/InterfaceInfoPal.Unix.cs @@ -65,7 +65,7 @@ public static unsafe uint InterfaceNameToIndex(ReadOnlySpan interf { if (nativeMemory != null) { - int errNo = Marshal.GetLastSystemError(); + int errNo = Marshal.GetLastPInvokeError(); NativeMemory.Free(nativeMemory); Marshal.SetLastPInvokeError(errNo); From 827e5f85d0f51516bfaadb06e9899e2289c1b554 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Fri, 18 Oct 2024 06:08:23 +0100 Subject: [PATCH 42/43] Performance optimisations * Replaced int.CreateTruncating with a JIT pattern which converts from TChar to a ushort. * Removed various TChar.CreateTruncating constants, reinstating direct character constants. * Replaced branch statements when parsing IPv4 numerics with a HexConverter lookup. * IPv4 parts stored in an array of longs to improve register usage. * Small improvement in reassembly to eliminate extra array access. * Reverted changes to Uri parsing layer, removing the translation logic between a Span and a pointer for IPv4 addresses. --- .../System/Net/IPv4AddressHelper.Common.cs | 88 +++++++++---------- .../System/Net/IPv6AddressHelper.Common.cs | 15 ++-- .../src/System/IPv4AddressHelper.cs | 57 ++++++------ .../src/System/IPv6AddressHelper.cs | 3 +- .../System.Private.Uri/src/System/Uri.cs | 2 +- 5 files changed, 80 insertions(+), 85 deletions(-) diff --git a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs index c5200796103db7..91243b31d24e20 100644 --- a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs @@ -4,6 +4,7 @@ using System.Buffers.Binary; using System.Diagnostics; using System.Numerics; +using System.Runtime.CompilerServices; namespace System.Net { @@ -18,6 +19,17 @@ internal static partial class IPv4AddressHelper private const int NumberOfLabels = 4; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ushort ToUShort(TChar value) + where TChar : unmanaged, IBinaryInteger + { + Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); + + return typeof(TChar) == typeof(char) + ? (char)(object)value + : (byte)(object)value; + } + // Only called from the IPv6Helper, only parse the canonical format internal static int ParseHostNumber(ReadOnlySpan str, int start, int end) where TChar : unmanaged, IBinaryInteger @@ -29,9 +41,9 @@ internal static int ParseHostNumber(ReadOnlySpan str, int start, i for (int i = 0; i < numbers.Length; ++i) { int b = 0; - int ch; + ushort ch; - for (; (start < end) && (ch = int.CreateTruncating(str[start])) != '.' && ch != ':'; ++start) + for (; (start < end) && (ch = ToUShort(str[start])) != '.' && ch != ':'; ++start) { b = (b * 10) + ch - '0'; } @@ -118,24 +130,23 @@ internal static unsafe bool IsValidCanonical(TChar* name, int start, ref Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); int dots = 0; - int number = 0; + long number = 0; bool haveNumber = false; bool firstCharIsZero = false; while (start < end) { - TChar ch = name[start]; + ushort ch = ToUShort(name[start]); if (allowIPv6) { // For an IPv4 address nested inside an IPv6 address, the terminator is either the IPv6 address terminator (']'), prefix ('/') or ScopeId ('%') - if (ch == TChar.CreateTruncating(']') || ch == TChar.CreateTruncating('/') || ch == TChar.CreateTruncating('%')) + if (ch == ']' || ch == '/' || ch == '%') { break; } } - else if (ch == TChar.CreateTruncating('/') || ch == TChar.CreateTruncating('\\') - || (notImplicitFile && (ch == TChar.CreateTruncating(':') || ch == TChar.CreateTruncating('?') || ch == TChar.CreateTruncating('#')))) + else if (ch == '/' || ch == '\\' || (notImplicitFile && (ch == ':' || ch == '?' || ch == '#'))) { // For a normal IPv4 address, the terminator is the prefix ('/' or its counterpart, '\'). If notImplicitFile is set, the terminator // is one of the characters which signify the start of the rest of the URI - the port number (':'), query string ('?') or fragment ('#') @@ -144,7 +155,7 @@ internal static unsafe bool IsValidCanonical(TChar* name, int start, ref } // An explicit cast to an unsigned integer forces character values preceding '0' to underflow, eliminating one comparison below. - uint parsedCharacter = uint.CreateTruncating(ch - TChar.CreateTruncating('0')); + ushort parsedCharacter = (ushort)(ch - '0'); if (parsedCharacter < IPv4AddressHelper.Decimal) { @@ -161,13 +172,13 @@ internal static unsafe bool IsValidCanonical(TChar* name, int start, ref } haveNumber = true; - number = number * IPv4AddressHelper.Decimal + (int)parsedCharacter; + number = number * IPv4AddressHelper.Decimal + parsedCharacter; if (number > byte.MaxValue) { return false; } } - else if (ch == TChar.CreateTruncating('.')) + else if (ch == '.') { // If the current character is not an integer, it may be the IPv4 component separator ('.') @@ -205,7 +216,8 @@ internal static unsafe long ParseNonCanonical(TChar* name, int start, ref Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); int numberBase = IPv4AddressHelper.Decimal; - uint* parts = stackalloc uint[4]; + ushort ch; + long* parts = stackalloc long[4]; long currentValue = 0; bool atLeastOneChar = false; @@ -215,7 +227,7 @@ internal static unsafe long ParseNonCanonical(TChar* name, int start, ref for (; current < end; current++) { - TChar ch = name[current]; + ch = ToUShort(name[current]); currentValue = 0; // Figure out what base this section is in, default to base 10. @@ -223,15 +235,15 @@ internal static unsafe long ParseNonCanonical(TChar* name, int start, ref // If the number starts with 0x, it should be interpreted in base 16 / hex numberBase = IPv4AddressHelper.Decimal; - if (ch == TChar.CreateTruncating('0')) + if (ch == '0') { current++; atLeastOneChar = true; if (current < end) { - ch = name[current]; + ch = ToUShort(name[current]); - if (ch == TChar.CreateTruncating('x') || ch == TChar.CreateTruncating('X')) + if (ch == 'x' || ch == 'X') { numberBase = IPv4AddressHelper.Hex; @@ -248,27 +260,10 @@ internal static unsafe long ParseNonCanonical(TChar* name, int start, ref // Parse this section for (; current < end; current++) { - ch = name[current]; - int digitValue; - int characterValue = int.CreateTruncating(ch); + ch = ToUShort(name[current]); + int digitValue = HexConverter.FromChar(ch); - if ((numberBase == IPv4AddressHelper.Decimal || numberBase == IPv4AddressHelper.Hex) && '0' <= characterValue && characterValue <= '9') - { - digitValue = characterValue - '0'; - } - else if (numberBase == IPv4AddressHelper.Octal && '0' <= characterValue && characterValue <= '7') - { - digitValue = characterValue - '0'; - } - else if (numberBase == IPv4AddressHelper.Hex && 'a' <= characterValue && characterValue <= 'f') - { - digitValue = characterValue + 10 - 'a'; - } - else if (numberBase == IPv4AddressHelper.Hex && 'A' <= characterValue && characterValue <= 'F') - { - digitValue = characterValue + 10 - 'A'; - } - else + if (digitValue >= numberBase) { break; // Invalid/terminator } @@ -282,7 +277,7 @@ internal static unsafe long ParseNonCanonical(TChar* name, int start, ref atLeastOneChar = true; } - if (current < end && ch == TChar.CreateTruncating('.')) + if (current < end && ch == '.') { if (dotCount >= 3 // Max of 3 dots and 4 segments || !atLeastOneChar // No empty segments: 1...1 @@ -291,7 +286,7 @@ internal static unsafe long ParseNonCanonical(TChar* name, int start, ref { return Invalid; } - parts[dotCount] = (uint)currentValue; + parts[dotCount] = currentValue; dotCount++; atLeastOneChar = false; continue; @@ -309,8 +304,7 @@ internal static unsafe long ParseNonCanonical(TChar* name, int start, ref { // end of string, allowed } - else if (name[current] == TChar.CreateTruncating('/') || name[current] == TChar.CreateTruncating('\\') - || (notImplicitFile && (name[current] == TChar.CreateTruncating(':') || name[current] == TChar.CreateTruncating('?') || name[current] == TChar.CreateTruncating('#')))) + else if ((ch = ToUShort(name[current])) == '/' || ch == '\\' || (notImplicitFile && (ch == ':' || ch == '?' || ch == '#'))) { // For a normal IPv4 address, the terminator is the prefix ('/' or its counterpart, '\'). If notImplicitFile is set, the terminator // is one of the characters which signify the start of the rest of the URI - the port number (':'), query string ('?') or fragment ('#') @@ -323,37 +317,35 @@ internal static unsafe long ParseNonCanonical(TChar* name, int start, ref return Invalid; } - parts[dotCount] = (uint)currentValue; - // Parsed, reassemble and check for overflows in the last part. Previous parts have already been checked in the loop switch (dotCount) { case 0: // 0xFFFFFFFF - return parts[0]; + return currentValue; case 1: // 0xFF.0xFFFFFF Debug.Assert(parts[0] <= 0xFF); - if (parts[1] > 0xffffff) + if (currentValue > 0xffffff) { return Invalid; } - return (parts[0] << 24) | parts[1]; + return (parts[0] << 24) | currentValue; case 2: // 0xFF.0xFF.0xFFFF Debug.Assert(parts[0] <= 0xFF); Debug.Assert(parts[1] <= 0xFF); - if (parts[2] > 0xffff) + if (currentValue > 0xffff) { return Invalid; } - return (parts[0] << 24) | (parts[1] << 16) | parts[2]; + return (parts[0] << 24) | (parts[1] << 16) | currentValue; case 3: // 0xFF.0xFF.0xFF.0xFF Debug.Assert(parts[0] <= 0xFF); Debug.Assert(parts[1] <= 0xFF); Debug.Assert(parts[2] <= 0xFF); - if (parts[3] > 0xff) + if (currentValue > 0xff) { return Invalid; } - return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]; + return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | currentValue; default: return Invalid; } diff --git a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs index 5c871c68a1a98a..37c73fc9862fdf 100644 --- a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs @@ -135,7 +135,7 @@ internal static unsafe bool IsValidStrict(TChar* name, int start, ref int int i; for (i = start; i < end; ++i) { - int currentCh = int.CreateTruncating(name[i]); + ushort currentCh = IPv4AddressHelper.ToUShort(name[i]); if (HexConverter.IsHexChar(currentCh)) { @@ -196,7 +196,7 @@ internal static unsafe bool IsValidStrict(TChar* name, int start, ref int i += 4; for (; i < end; i++) { - int ch = int.CreateTruncating(name[i]); + ushort ch = IPv4AddressHelper.ToUShort(name[i]); if (!HexConverter.IsHexChar(ch)) { @@ -209,7 +209,7 @@ internal static unsafe bool IsValidStrict(TChar* name, int start, ref int i += 2; for (; i < end; i++) { - if (uint.CreateTruncating(name[i] - TChar.CreateTruncating('0')) >= IPv6AddressHelper.Decimal) + if ((uint)(IPv4AddressHelper.ToUShort(name[i]) - '0') >= IPv6AddressHelper.Decimal) { return false; } @@ -315,7 +315,7 @@ internal static void Parse(ReadOnlySpan address, scoped Span(ReadOnlySpan address, scoped Span(ReadOnlySpan address, scoped Span str, ref bool isLoopback) + internal static string ParseCanonicalName(string str, int start, int end, ref bool isLoopback) { - Span numbers = stackalloc byte[NumberOfLabels]; - isLoopback = Parse(str, numbers); - - Span stackSpace = stackalloc char[NumberOfLabels * 3 + 3]; - int totalChars = 0, charsWritten; - for (int i = 0; i < 3; i++) + unsafe { - numbers[i].TryFormat(stackSpace.Slice(totalChars), out charsWritten); - int periodPos = totalChars + charsWritten; - stackSpace[periodPos] = '.'; - totalChars = periodPos + 1; + byte* numbers = stackalloc byte[NumberOfLabels]; + isLoopback = Parse(str, numbers, start, end); + + Span stackSpace = stackalloc char[NumberOfLabels * 3 + 3]; + int totalChars = 0, charsWritten; + for (int i = 0; i < 3; i++) + { + numbers[i].TryFormat(stackSpace.Slice(totalChars), out charsWritten); + int periodPos = totalChars + charsWritten; + stackSpace[periodPos] = '.'; + totalChars = periodPos + 1; + } + numbers[3].TryFormat(stackSpace.Slice(totalChars), out charsWritten); + return new string(stackSpace.Slice(0, totalChars + charsWritten)); } - numbers[3].TryFormat(stackSpace.Slice(totalChars), out charsWritten); - return new string(stackSpace.Slice(0, totalChars + charsWritten)); } // @@ -37,19 +37,24 @@ internal static string ParseCanonicalName(ReadOnlySpan str, ref bool isLoo // // Convert this IPv4 address into a sequence of 4 8-bit numbers // - private static unsafe bool Parse(ReadOnlySpan name, Span numbers) + private static unsafe bool Parse(string name, byte* numbers, int start, int end) { - int changedEnd = name.Length; - long result; - - fixed (char* ipString = &MemoryMarshal.GetReference(name)) + fixed (char* ipString = name) { - // "name" parameter includes ports, so changedEnd may be different from span length - result = ParseNonCanonical(ipString, 0, ref changedEnd, true); - } - Debug.Assert(result != Invalid, $"Failed to parse after already validated: {name}"); + // end includes ports, so changedEnd may be different from end + int changedEnd = end; + long result = IPv4AddressHelper.ParseNonCanonical(ipString, start, ref changedEnd, true); + + Debug.Assert(result != Invalid, $"Failed to parse after already validated: {name}"); - BinaryPrimitives.WriteUInt32BigEndian(numbers, (uint)result); + unchecked + { + numbers[0] = (byte)(result >> 24); + numbers[1] = (byte)(result >> 16); + numbers[2] = (byte)(result >> 8); + numbers[3] = (byte)(result); + } + } return numbers[0] == 127; } diff --git a/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs b/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs index 5637e552593a96..ed1dada257b44b 100644 --- a/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs +++ b/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Numerics; namespace System.Net { @@ -88,7 +87,7 @@ internal static string ParseCanonicalName(ReadOnlySpan str, ref bool isLoo return new string(stackSpace.Slice(0, pos)); } - private static bool IsLoopback(ReadOnlySpan numbers) + private static unsafe bool IsLoopback(ReadOnlySpan numbers) { // // is the address loopback? Loopback is defined as one of: diff --git a/src/libraries/System.Private.Uri/src/System/Uri.cs b/src/libraries/System.Private.Uri/src/System/Uri.cs index a5600ecfb2b10d..a48a5f0f71149f 100644 --- a/src/libraries/System.Private.Uri/src/System/Uri.cs +++ b/src/libraries/System.Private.Uri/src/System/Uri.cs @@ -2520,7 +2520,7 @@ private static string CreateHostStringHelper(string str, int idx, int end, ref F break; case Flags.IPv4HostType: - host = IPv4AddressHelper.ParseCanonicalName(str.AsSpan(idx), ref loopback); + host = IPv4AddressHelper.ParseCanonicalName(str, idx, end, ref loopback); break; case Flags.UncHostType: From dfb68d2d5d07c6d21591dfbd563d2eea3129167b Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Wed, 23 Oct 2024 05:21:18 +0100 Subject: [PATCH 43/43] Following code review * Replaced ushorts with integers to remove some zero-extensions. * Replaced NativeMemory with an ArrayPool. * Removed unnecessary assignment to ch. --- .../System/Net/IPv4AddressHelper.Common.cs | 16 +++++++------- .../System/Net/IPv6AddressHelper.Common.cs | 21 +++++++++---------- .../InterfaceInfoPal.Unix.cs | 17 +++++++-------- 3 files changed, 26 insertions(+), 28 deletions(-) diff --git a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs index 91243b31d24e20..a3557b2e73e935 100644 --- a/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs @@ -26,8 +26,8 @@ internal static ushort ToUShort(TChar value) Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); return typeof(TChar) == typeof(char) - ? (char)(object)value - : (byte)(object)value; + ? (char)(object)value + : (byte)(object)value; } // Only called from the IPv6Helper, only parse the canonical format @@ -41,7 +41,7 @@ internal static int ParseHostNumber(ReadOnlySpan str, int start, i for (int i = 0; i < numbers.Length; ++i) { int b = 0; - ushort ch; + int ch; for (; (start < end) && (ch = ToUShort(str[start])) != '.' && ch != ':'; ++start) { @@ -136,7 +136,7 @@ internal static unsafe bool IsValidCanonical(TChar* name, int start, ref while (start < end) { - ushort ch = ToUShort(name[start]); + int ch = ToUShort(name[start]); if (allowIPv6) { @@ -155,7 +155,7 @@ internal static unsafe bool IsValidCanonical(TChar* name, int start, ref } // An explicit cast to an unsigned integer forces character values preceding '0' to underflow, eliminating one comparison below. - ushort parsedCharacter = (ushort)(ch - '0'); + uint parsedCharacter = (uint)(ch - '0'); if (parsedCharacter < IPv4AddressHelper.Decimal) { @@ -216,8 +216,8 @@ internal static unsafe long ParseNonCanonical(TChar* name, int start, ref Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); int numberBase = IPv4AddressHelper.Decimal; - ushort ch; - long* parts = stackalloc long[4]; + int ch = 0; + long* parts = stackalloc long[3]; // One part per octet. Final octet doesn't have a terminator, so is stored in currentValue. long currentValue = 0; bool atLeastOneChar = false; @@ -304,7 +304,7 @@ internal static unsafe long ParseNonCanonical(TChar* name, int start, ref { // end of string, allowed } - else if ((ch = ToUShort(name[current])) == '/' || ch == '\\' || (notImplicitFile && (ch == ':' || ch == '?' || ch == '#'))) + else if (ch == '/' || ch == '\\' || (notImplicitFile && (ch == ':' || ch == '?' || ch == '#'))) { // For a normal IPv4 address, the terminator is the prefix ('/' or its counterpart, '\'). If notImplicitFile is set, the terminator // is one of the characters which signify the start of the rest of the URI - the port number (':'), query string ('?') or fragment ('#') diff --git a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs index 37c73fc9862fdf..6645daa83e4b20 100644 --- a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs @@ -8,7 +8,6 @@ namespace System.Net { internal static partial class IPv6AddressHelper { - private const int Decimal = 10; private const int Hex = 16; private const int NumberOfLabels = 8; @@ -135,7 +134,7 @@ internal static unsafe bool IsValidStrict(TChar* name, int start, ref int int i; for (i = start; i < end; ++i) { - ushort currentCh = IPv4AddressHelper.ToUShort(name[i]); + int currentCh = IPv4AddressHelper.ToUShort(name[i]); if (HexConverter.IsHexChar(currentCh)) { @@ -196,7 +195,7 @@ internal static unsafe bool IsValidStrict(TChar* name, int start, ref int i += 4; for (; i < end; i++) { - ushort ch = IPv4AddressHelper.ToUShort(name[i]); + int ch = IPv4AddressHelper.ToUShort(name[i]); if (!HexConverter.IsHexChar(ch)) { @@ -209,7 +208,7 @@ internal static unsafe bool IsValidStrict(TChar* name, int start, ref int i += 2; for (; i < end; i++) { - if ((uint)(IPv4AddressHelper.ToUShort(name[i]) - '0') >= IPv6AddressHelper.Decimal) + if (!char.IsAsciiDigit((char)IPv4AddressHelper.ToUShort(name[i]))) { return false; } @@ -314,8 +313,8 @@ internal static void Parse(ReadOnlySpan address, scoped Span(ReadOnlySpan address, scoped Span(ReadOnlySpan address, scoped Span(ReadOnlySpan address, scoped Span(ReadOnlySpan address, scoped Span(ReadOnlySpan address, scoped Span(ReadOnlySpan interf // Measured in bytes, including null terminator. int bufferSize = 0; - byte* nativeMemory = null; + byte[]? rentedBuffer = null; // The underlying API for this method accepts a null-terminated UTF8 string containing the interface name. // If TChar is byte, the only work required is a byte copy. If TChar is char, there's a transcoding step from Unicode @@ -41,9 +42,10 @@ public static unsafe uint InterfaceNameToIndex(ReadOnlySpan interf try { - nativeMemory = bufferSize <= StackAllocationThreshold ? null : (byte*)NativeMemory.Alloc((nuint)bufferSize); - - Span buffer = nativeMemory == null ? stackalloc byte[StackAllocationThreshold].Slice(0, bufferSize) : new Span(nativeMemory, bufferSize); + Span buffer = (bufferSize <= StackAllocationThreshold + ? stackalloc byte[StackAllocationThreshold] + : (rentedBuffer = ArrayPool.Shared.Rent(bufferSize))) + .Slice(0, bufferSize); if (typeof(TChar) == typeof(byte)) { @@ -63,12 +65,9 @@ public static unsafe uint InterfaceNameToIndex(ReadOnlySpan interf } finally { - if (nativeMemory != null) + if (rentedBuffer != null) { - int errNo = Marshal.GetLastPInvokeError(); - - NativeMemory.Free(nativeMemory); - Marshal.SetLastPInvokeError(errNo); + ArrayPool.Shared.Return(rentedBuffer); } } }