Skip to content

Add IPNetwork.Parse and TryParse #44573

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

Merged
merged 6 commits into from
Oct 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
119 changes: 111 additions & 8 deletions src/Middleware/HttpOverrides/src/IPNetwork.cs
Original file line number Diff line number Diff line change
@@ -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.CodeAnalysis;
using System.Net;
using System.Net.Sockets;

Expand All @@ -16,9 +17,18 @@ public class IPNetwork
/// </summary>
/// <param name="prefix">The <see cref="IPAddress"/>.</param>
/// <param name="prefixLength">The prefix length.</param>
public IPNetwork(IPAddress prefix, int prefixLength)
/// <exception cref="ArgumentOutOfRangeException"><paramref name="prefixLength"/> is out of range.</exception>
public IPNetwork(IPAddress prefix, int prefixLength) : this(prefix, prefixLength, true)
{
CheckPrefixLengthRange(prefix, prefixLength);
}

private IPNetwork(IPAddress prefix, int prefixLength, bool checkPrefixLengthRange)
{
if (checkPrefixLengthRange &&
!IsValidPrefixLengthRange(prefix, prefixLength))
{
throw new ArgumentOutOfRangeException(nameof(prefixLength), "The prefix length was out of range.");
}

Prefix = prefix;
PrefixLength = prefixLength;
Expand Down Expand Up @@ -83,21 +93,114 @@ private byte[] CreateMask()
return mask;
}

private static void CheckPrefixLengthRange(IPAddress prefix, int prefixLength)
private static bool IsValidPrefixLengthRange(IPAddress prefix, int prefixLength)
{
if (prefixLength < 0)
{
throw new ArgumentOutOfRangeException(nameof(prefixLength));
return false;
}

if (prefix.AddressFamily == AddressFamily.InterNetwork && prefixLength > 32)
return prefix.AddressFamily switch
{
AddressFamily.InterNetwork => prefixLength <= 32,
AddressFamily.InterNetworkV6 => prefixLength <= 128,
_ => true
};
}

/// <summary>
/// Converts the specified <see cref="ReadOnlySpan{T}"/> of <see langword="char"/> representation of
/// an IP address and a prefix length to its <see cref="IPNetwork"/> equivalent.
/// </summary>
/// <param name="networkSpan">The <see cref="ReadOnlySpan{T}"/> of <see langword="char"/> to convert, in CIDR notation.</param>
/// <returns>
///The <see cref="IPNetwork"/> equivalent to the IP address and prefix length contained in <paramref name="networkSpan"/>.
/// </returns>
/// <exception cref="FormatException"><paramref name="networkSpan"/> is not in the correct format.</exception>
/// <exception cref="ArgumentOutOfRangeException">The prefix length contained in <paramref name="networkSpan"/> is out of range.</exception>
/// <inheritdoc cref="TryParseComponents(ReadOnlySpan{char}, out IPAddress?, out int)"/>
public static IPNetwork Parse(ReadOnlySpan<char> networkSpan)
{
if (!TryParseComponents(networkSpan, out var prefix, out var prefixLength))
{
throw new ArgumentOutOfRangeException(nameof(prefixLength));
throw new FormatException("An invalid IP address or prefix length was specified.");
}

if (prefix.AddressFamily == AddressFamily.InterNetworkV6 && prefixLength > 128)
if (!IsValidPrefixLengthRange(prefix, prefixLength))
{
throw new ArgumentOutOfRangeException(nameof(prefixLength));
throw new ArgumentOutOfRangeException(nameof(networkSpan), "The prefix length was out of range.");
}

return new IPNetwork(prefix, prefixLength, false);
}

/// <summary>
/// Converts the specified <see cref="ReadOnlySpan{T}"/> of <see langword="char"/> representation of
/// an IP address and a prefix length to its <see cref="IPNetwork"/> equivalent, and returns a value
/// that indicates whether the conversion succeeded.
/// </summary>
/// <param name="networkSpan">The <see cref="ReadOnlySpan{T}"/> of <see langword="char"/> to validate.</param>
/// <param name="network">
/// When this method returns, contains the <see cref="IPNetwork"/> equivalent to the IP Address
/// and prefix length contained in <paramref name="networkSpan"/>, if the conversion succeeded,
/// or <see langword="null"/> if the conversion failed. This parameter is passed uninitialized.
/// </param>
/// <returns>
/// <see langword="true"/> if the <paramref name="networkSpan"/> parameter was
/// converted successfully; otherwise <see langword="false"/>.
/// </returns>
/// <inheritdoc cref="TryParseComponents(ReadOnlySpan{char}, out IPAddress?, out int)"/>
public static bool TryParse(ReadOnlySpan<char> networkSpan, [NotNullWhen(true)] out IPNetwork? network)
{
network = null;

if (!TryParseComponents(networkSpan, out var prefix, out var prefixLength))
{
return false;
}

if (!IsValidPrefixLengthRange(prefix, prefixLength))
{
return false;
}

network = new IPNetwork(prefix, prefixLength, false);
return true;
}

/// <remarks>
/// <para>
/// The specified representation must be expressed using CIDR (Classless Inter-Domain Routing) notation, or 'slash notation',
/// which contains an IPv4 or IPv6 address and the subnet mask prefix length, separated by a forward slash.
/// </para>
/// <example>
/// e.g. <c>"192.168.0.1/31"</c> for IPv4, <c>"2001:db8:3c4d::1/127"</c> for IPv6
/// </example>
/// </remarks>
private static bool TryParseComponents(
ReadOnlySpan<char> networkSpan,
[NotNullWhen(true)] out IPAddress? prefix,
out int prefixLength)
{
prefix = null;
prefixLength = default;

var forwardSlashIndex = networkSpan.IndexOf('/');
if (forwardSlashIndex < 0)
{
return false;
}

if (!IPAddress.TryParse(networkSpan.Slice(0, forwardSlashIndex), out prefix))
{
return false;
}

if (!int.TryParse(networkSpan.Slice(forwardSlashIndex + 1), out prefixLength))
{
return false;
}

return true;
}
}
2 changes: 2 additions & 0 deletions src/Middleware/HttpOverrides/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
static Microsoft.AspNetCore.HttpOverrides.IPNetwork.Parse(System.ReadOnlySpan<char> networkSpan) -> Microsoft.AspNetCore.HttpOverrides.IPNetwork!
static Microsoft.AspNetCore.HttpOverrides.IPNetwork.TryParse(System.ReadOnlySpan<char> networkSpan, out Microsoft.AspNetCore.HttpOverrides.IPNetwork? network) -> bool
155 changes: 155 additions & 0 deletions src/Middleware/HttpOverrides/test/IPNetworkTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,159 @@ public void Contains_Negative(string prefixText, int length, string addressText)
var network = new IPNetwork(IPAddress.Parse(prefixText), length);
Assert.False(network.Contains(IPAddress.Parse(addressText)));
}

[Theory]
[InlineData("192.168.1.1", 0)]
[InlineData("192.168.1.1", 32)]
[InlineData("2001:db8:3c4d::1", 0)]
[InlineData("2001:db8:3c4d::1", 128)]
public void Ctor_WithValidFormat_IsSuccessfullyCreated(string prefixText, int prefixLength)
{
// Arrange
var address = IPAddress.Parse(prefixText);

// Act
var network = new IPNetwork(address, prefixLength);

// Assert
Assert.Equal(prefixText, network.Prefix.ToString());
Assert.Equal(prefixLength, network.PrefixLength);
}

[Theory]
[InlineData("192.168.1.1", -1)]
[InlineData("192.168.1.1", 33)]
[InlineData("2001:db8:3c4d::1", -1)]
[InlineData("2001:db8:3c4d::1", 129)]
public void Ctor_WithPrefixLengthOutOfRange_ThrowsArgumentOutOfRangeException(string prefixText, int prefixLength)
{
// Arrange
var address = IPAddress.Parse(prefixText);

// Act
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new IPNetwork(address, prefixLength));

// Assert
Assert.StartsWith("The prefix length was out of range.", ex.Message);
}

[Theory]
[MemberData(nameof(ValidPrefixWithPrefixLengthData))]
public void Parse_WithValidFormat_ParsedCorrectly(string input, string expectedPrefix, int expectedPrefixLength)
{
// Act
var network = IPNetwork.Parse(input);

// Assert
Assert.Equal(expectedPrefix, network.Prefix.ToString());
Assert.Equal(expectedPrefixLength, network.PrefixLength);
}

[Theory]
[InlineData(null)]
[MemberData(nameof(InvalidPrefixOrPrefixLengthData))]
public void Parse_WithInvalidFormat_ThrowsFormatException(string input)
{
// Arrange & Act & Assert
var ex = Assert.Throws<FormatException>(() => IPNetwork.Parse(input));
Assert.Equal("An invalid IP address or prefix length was specified.", ex.Message);
}

[Theory]
[MemberData(nameof(PrefixLengthOutOfRangeData))]
public void Parse_WithOutOfRangePrefixLength_ThrowsArgumentOutOfRangeException(string input)
{
// Arrange & Act & Assert
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => IPNetwork.Parse(input));
Assert.StartsWith("The prefix length was out of range.", ex.Message);
}

[Theory]
[MemberData(nameof(ValidPrefixWithPrefixLengthData))]
public void TryParse_WithValidFormat_ParsedCorrectly(string input, string expectedPrefix, int expectedPrefixLength)
{
// Act
var result = IPNetwork.TryParse(input, out var network);

// Assert
Assert.True(result);
Assert.NotNull(network);
Assert.Equal(expectedPrefix, network.Prefix.ToString());
Assert.Equal(expectedPrefixLength, network.PrefixLength);
}

[Theory]
[InlineData(null)]
[MemberData(nameof(InvalidPrefixOrPrefixLengthData))]
[MemberData(nameof(PrefixLengthOutOfRangeData))]
public void TryParse_WithInvalidFormat_ReturnsFalse(string input)
{
// Act
var result = IPNetwork.TryParse(input, out var network);

// Assert
Assert.False(result);
Assert.Null(network);
}

public static TheoryData<string, string, int> ValidPrefixWithPrefixLengthData() => new()
{
// IPv4
{ "10.1.0.0/16", "10.1.0.0", 16 },
{ "10.1.1.0/8", "10.1.1.0", 8 },
{ "174.0.0.0/7", "174.0.0.0", 7 },
{ "10.174.0.0/15", "10.174.0.0", 15 },
{ "10.168.0.0/14", "10.168.0.0", 14 },
{ "192.168.0.1/31", "192.168.0.1", 31 },
{ "192.168.0.1/31", "192.168.0.1", 31 },
{ "192.168.0.1/32", "192.168.0.1", 32 },
{ "192.168.1.1/0", "192.168.1.1", 0 },
{ "192.168.1.1/0", "192.168.1.1", 0 },

// IPv6
{ "2001:db8:3c4d::/127", "2001:db8:3c4d::", 127 },
{ "2001:db8:3c4d::1/128", "2001:db8:3c4d::1", 128 },
{ "2001:db8:3c4d::1/0", "2001:db8:3c4d::1", 0 },
{ "2001:db8:3c4d::1/0", "2001:db8:3c4d::1", 0 }
};

public static TheoryData<string> InvalidPrefixOrPrefixLengthData() => new()
{
string.Empty,
"abcdefg",

// Missing forward slash
"10.1.0.016",
"2001:db8:3c4d::1127",

// Invalid prefix
"/16",
"10.1./16",
"10.1.0./16",
"10.1.ABC.0/16",
"200123:db8:3c4d::/127",
":db8:3c4d::/127",
"2001:?:3c4d::1/0",

// Invalid prefix length
"10.1.0.0/",
"10.1.0.0/16-",
"10.1.0.0/ABC",
"2001:db8:3c4d::/",
"2001:db8:3c4d::1/128-",
"2001:db8:3c4d::1/ABC"
};

public static TheoryData<string> PrefixLengthOutOfRangeData() => new()
{
// Negative prefix length
"10.1.0.0/-16",
"2001:db8:3c4d::/-127",

// Prefix length out of range (IPv4)
"10.1.0.0/33",

// Prefix length out of range (IPv6)
"2001:db8:3c4d::/129"
};
}