Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement IUtf8SpanParsable on IPAddress and IPNetwork #102144

Open
wants to merge 42 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
81ab847
Changed IP address parsers to use generics
edwardneal May 6, 2024
2bf97eb
Moved generic type definitions to parser classes
edwardneal May 6, 2024
223cece
Implemented IUtf8SpanParsable
edwardneal May 6, 2024
b9e3a62
System.Private.Uri changes
edwardneal May 7, 2024
4e580ac
Post-Uri testing, corrected additional uses of IPvXAddressHelper
edwardneal May 13, 2024
8ff7241
Added IUtf8SpanParsable unit tests
edwardneal May 13, 2024
f627e44
Implemented IUtf8SpanParsable on IPNetwork, added tests
edwardneal May 13, 2024
1f52945
Brief tidy-up of System.Net.Primitives ref project and csproj
edwardneal May 13, 2024
3710a7a
Further cleanup of System.Net.Primitives.Pal.Tests csproj
edwardneal May 13, 2024
1f4968c
Further cleanup of System.Net.Primitives.UnitTests.Tests csproj
edwardneal May 13, 2024
ed8c666
Correctly setting bytesConsumed in IPv4AddressHelper.ParseNonCanonical
edwardneal May 13, 2024
3c6e190
Merge branch 'dotnet:main' into issue-81500-ipaddress-ipnetwork
edwardneal Jun 14, 2024
ea95067
Changes following API review
edwardneal Jun 14, 2024
d478e82
Code review changes (round 1)
edwardneal Jun 19, 2024
c42c706
Removed generic type definition from classes
edwardneal Jun 22, 2024
49bc3a5
Replaced ref parameter with out parameter, propagated
edwardneal Jun 22, 2024
3378947
Addressing @jkotas code review comments
edwardneal Jun 23, 2024
66ec8c2
Inlined all constant-value variables
edwardneal Jun 23, 2024
afdee99
Swapped CreateChecked to CreateTruncating
edwardneal Jun 23, 2024
043120f
Code review feedback: initial work
edwardneal Jun 23, 2024
c593694
Removed unnecessary lastSequence modification
edwardneal Jun 23, 2024
4aff60e
Code review changes
edwardneal Jul 2, 2024
8c41d9b
Optimisations to IPv[4/6]AddressHelper
edwardneal Jul 4, 2024
64c639c
Cleaned up IPAddressParser.Common.cs
edwardneal Jul 4, 2024
f290a3a
.sln file cleanup
edwardneal Jul 4, 2024
65027db
Testing micro-optimisations in IPv4 address parser
edwardneal Jul 6, 2024
cd73915
Added "in" qualifier to Span/ROS parameters
edwardneal Jul 6, 2024
385ee62
Correcting trailing "in" reference
edwardneal Jul 6, 2024
f7d4298
Reverted addition of "in" modifier
edwardneal Jul 6, 2024
cb1d208
Code review
edwardneal Jul 12, 2024
0f3bbaf
Additional optimisations post-benchmark
edwardneal Jul 12, 2024
1f8a166
Performance improvement
edwardneal Jul 15, 2024
21f1f86
Changes following code review
edwardneal Jul 17, 2024
70a60a5
Initial response to newest review
edwardneal Jul 20, 2024
e0c49c2
Reverted hexadecimal prefix to be case-sensitive
edwardneal Jul 20, 2024
94b2dd1
Flowed Spans through to PInvoke
edwardneal Jul 21, 2024
02a7a84
PInvoke/PAL cleanup
edwardneal Jul 22, 2024
c352e01
Added assertion
edwardneal Jul 22, 2024
2da63cb
Replaced usage of if_nametoindex
edwardneal Jul 28, 2024
1ba7107
Moved interface name tests
edwardneal Jul 30, 2024
038d3a6
Corrected PInvoke signatures and Unix InterfaceInfoPal
edwardneal Oct 5, 2024
b79a6f5
Continued work with code review feedback & benchmark
edwardneal Oct 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ 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<byte> utf8NullTerminatedName);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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([MarshalAs(UnmanagedType.LPStr)] string name);
[LibraryImport(Libraries.IpHlpApi, SetLastError = true)]
internal static partial uint ConvertInterfaceLuidToIndex(in ulong interfaceLuid, ref uint ifIndex);
}
}
Original file line number Diff line number Diff line change
@@ -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<char> name, int nameLength);
}
}
Original file line number Diff line number Diff line change
@@ -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<char> unicodeNullTerminatedName, ref ulong interfaceLuid);
edwardneal marked this conversation as resolved.
Show resolved Hide resolved
}
}
138 changes: 78 additions & 60 deletions src/libraries/Common/src/System/Net/IPv4AddressHelper.Common.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,36 @@
// 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 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<char> str, int start, int end)
internal static int ParseHostNumber<TChar>(ReadOnlySpan<TChar> str, int start, int end)
where TChar : unmanaged, IBinaryInteger<TChar>
{
Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

Span<byte> numbers = stackalloc byte[NumberOfLabels];

for (int i = 0; i < numbers.Length; ++i)
{
int b = 0;
char ch;
int ch;

for (; (start < end) && (ch = str[start]) != '.' && ch != ':'; ++start)
for (; (start < end) && (ch = int.CreateTruncating(str[start])) != '.' && ch != ':'; ++start)
{
b = (b * 10) + ch - '0';
}
Expand Down Expand Up @@ -79,7 +85,8 @@ internal static int ParseHostNumber(ReadOnlySpan<char> 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 unsafe bool IsValid<TChar>(TChar* name, int start, ref int end, bool allowIPv6, bool notImplicitFile, bool unknownScheme)
where TChar : unmanaged, IBinaryInteger<TChar>
{
// IPv6 can only have canonical IPv4 embedded. Unknown schemes will not attempt parsing of non-canonical IPv4 addresses.
if (allowIPv6 || unknownScheme)
Expand All @@ -105,32 +112,46 @@ 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 unsafe bool IsValidCanonical<TChar>(TChar* name, int start, ref int end, bool allowIPv6, bool notImplicitFile)
where TChar : unmanaged, IBinaryInteger<TChar>
{
Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

int dots = 0;
int number = 0;
bool haveNumber = false;
bool firstCharIsZero = false;

while (start < end)
{
char ch = name[start];
TChar ch = name[start];

if (allowIPv6)
{
// for ipv4 inside ipv6 the terminator is either ScopeId, prefix or ipv6 terminator
if (ch == ']' || ch == '/' || ch == '%')
// 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;
}
}
else if (ch == '/' || ch == '\\' || (notImplicitFile && (ch == ':' || ch == '?' || ch == '#')))
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;
}

if (char.IsAsciiDigit(ch))
// 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 < IPv4AddressHelper.Decimal)
{
if (!haveNumber && (ch == '0'))
// A number starting with zero should be interpreted in base 8 / octal
if (!haveNumber && parsedCharacter == 0)
{
if ((start + 1 < end) && name[start + 1] == '0')
if ((start + 1 < end) && name[start + 1] == TChar.CreateTruncating('0'))
{
// 00 is not allowed as a prefix.
return false;
Expand All @@ -140,14 +161,16 @@ 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 * IPv4AddressHelper.Decimal + (int)parsedCharacter;
if (number > byte.MaxValue)
{
return false;
}
}
else if (ch == '.')
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.
Expand Down Expand Up @@ -176,68 +199,63 @@ 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 unsafe long ParseNonCanonical<TChar>(TChar* name, int start, ref int end, bool notImplicitFile)
where TChar : unmanaged, IBinaryInteger<TChar>
{
int numberBase = Decimal;
char ch;
long* parts = stackalloc long[4];
Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

int numberBase = IPv4AddressHelper.Decimal;
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 = start;

for (; current < end; current++)
{
ch = name[current];
TChar ch = name[current];
currentValue = 0;

// Figure out what base this section is in
numberBase = Decimal;
if (ch == '0')
// 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'))
{
numberBase = Octal;
edwardneal marked this conversation as resolved.
Show resolved Hide resolved
current++;
atLeastOneChar = true;
if (current < end)
{
ch = name[current];
if (ch == 'x' || ch == 'X')

if (ch == TChar.CreateTruncating('x') || ch == TChar.CreateTruncating('X'))
{
numberBase = Hex;
numberBase = IPv4AddressHelper.Hex;

current++;
atLeastOneChar = false;
}
else
{
numberBase = IPv4AddressHelper.Octal;
}
}
}

// Parse this section
for (; current < end; current++)
{
ch = name[current];
int digitValue;
int characterValue = int.CreateTruncating(ch);
int digitValue = HexConverter.FromChar(characterValue);

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 (digitValue >= numberBase)
{
break; // Invalid/terminator
}

currentValue = (currentValue * numberBase) + digitValue;

if (currentValue > MaxIPv4Value) // Overflow
Expand All @@ -248,21 +266,21 @@ internal static unsafe long ParseNonCanonical(char* name, int start, ref int end
atLeastOneChar = true;
}

if (current < end && name[current] == '.')
if (current < end && ch == 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)
{
return Invalid;
}
parts[dotCount] = currentValue;
parts[dotCount] = (uint)currentValue;
dotCount++;
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;
}

Expand All @@ -275,8 +293,12 @@ internal static unsafe long ParseNonCanonical(char* name, int start, ref int end
{
// end of string, allowed
}
else if ((ch = name[current]) == '/' || ch == '\\' || (notImplicitFile && (ch == ':' || ch == '?' || ch == '#')))
else if (name[current] == TChar.CreateTruncating('/') || name[current] == TChar.CreateTruncating('\\')
|| (notImplicitFile && (name[current] == TChar.CreateTruncating(':') || name[current] == TChar.CreateTruncating('?') || name[current] == 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 ('#')

end = current;
}
else
Expand All @@ -285,35 +307,31 @@ internal static unsafe long ParseNonCanonical(char* name, int start, ref int end
return Invalid;
}

parts[dotCount] = currentValue;
parts[dotCount] = (uint)currentValue;

// 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
if (parts[0] > MaxIPv4Value)
{
return Invalid;
}
return parts[0];
case 1: // 0xFF.0xFFFFFF
if (parts[1] > 0xffffff)
{
return Invalid;
}
return (parts[0] << 24) | (parts[1] & 0xffffff);
return (parts[0] << 24) | parts[1];
edwardneal marked this conversation as resolved.
Show resolved Hide resolved
case 2: // 0xFF.0xFF.0xFFFF
if (parts[2] > 0xffff)
{
return Invalid;
}
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;
}
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;
}
Expand Down
Loading
Loading