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

Add IPNetwork.Parse and TryParse #44573

Merged
merged 6 commits into from
Oct 18, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
160 changes: 154 additions & 6 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,159 @@ 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)
{
throw new ArgumentOutOfRangeException(nameof(prefixLength));
return false;
}

if (prefix.AddressFamily == AddressFamily.InterNetworkV6 && prefixLength > 128)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrleated to your change: but these should be if-else if-else to avoid double-checks, or actually I'd write it as

static bool IsValidPrefixLengthRange1(IPAddress prefix, int prefixLength)
{
    return prefix.AddressFamily switch
    {
        AddressFamily.InterNetwork => prefixLength <= 32,
        AddressFamily.InterNetworkV6 => prefixLength <= 128,
        _ => prefixLength >= 0
    };
}

and let Roslyn and JIT optimize it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that's a good point. Although with your example, the prefixLength >= 0 check isn't be enforced for the non-default cases so negative prefixLength values would slip through. How about this instead?

static bool IsValidPrefixLengthRange(IPAddress prefix, int prefixLength)
{
+   if (prefixLength < 0)
+   {
+       return false;
+   }

    return prefix.AddressFamily switch
    {
        AddressFamily.InterNetwork => prefixLength <= 32,
        AddressFamily.InterNetworkV6 => prefixLength <= 128,
-       _ => prefixLength >= 0
+      _ => true
    };
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, you're right 👍🏻

{
throw new ArgumentOutOfRangeException(nameof(prefixLength));
return false;
}

return true;
}

/// <summary>
/// Converts the specified <see langword="string"/> representation of an IP address
/// and a prefix length to its <see cref="IPNetwork"/> equivalent.
/// </summary>
/// <param name="networkString">The <see langword="string"/> to convert, in CIDR notation.</param>
/// <returns>
/// The <see cref="IPNetwork"/> equivalent to the IP address and prefix length contained in <paramref name="networkString"/>.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="networkString"/> is <see langword="null"/>.</exception>
/// <exception cref="FormatException"><paramref name="networkString"/> is not in the correct format.</exception>
/// <exception cref="ArgumentOutOfRangeException">The prefix length contained in <paramref name="networkString"/> is out of range.</exception>
/// <inheritdoc cref="TryParseComponents(ReadOnlySpan{char}, out IPAddress?, out int)"/>
public static IPNetwork Parse(string? networkString)
Tratcher marked this conversation as resolved.
Show resolved Hide resolved
{
ArgumentNullException.ThrowIfNull(networkString);

return Parse(networkString.AsSpan());
}

/// <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 FormatException("An invalid IP address or prefix length was specified.");
}

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

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

/// <summary>
/// Converts the specified <see langword="string"/> 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="networkString">The <see langword="string"/> 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="networkString"/>, 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="networkString"/> parameter was
/// converted successfully; otherwise <see langword="false"/>.
/// </returns>
/// <inheritdoc cref="TryParseComponents(ReadOnlySpan{char}, out IPAddress?, out int)"/>
public static bool TryParse([NotNullWhen(true)] string? networkString, [NotNullWhen(true)] out IPNetwork? network)
{
return TryParse(networkString.AsSpan(), out network);
}

/// <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 == -1)
Tratcher marked this conversation as resolved.
Show resolved Hide resolved
{
return false;
}

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

if (!int.TryParse(networkSpan[(forwardSlashIndex + 1)..], out prefixLength))
Tratcher marked this conversation as resolved.
Show resolved Hide resolved
{
return false;
}

return true;
}
}
4 changes: 4 additions & 0 deletions src/Middleware/HttpOverrides/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
#nullable enable
static Microsoft.AspNetCore.HttpOverrides.IPNetwork.Parse(string? networkString) -> Microsoft.AspNetCore.HttpOverrides.IPNetwork!
static Microsoft.AspNetCore.HttpOverrides.IPNetwork.Parse(System.ReadOnlySpan<char> networkSpan) -> Microsoft.AspNetCore.HttpOverrides.IPNetwork!
static Microsoft.AspNetCore.HttpOverrides.IPNetwork.TryParse(string? networkString, out Microsoft.AspNetCore.HttpOverrides.IPNetwork? network) -> bool
static Microsoft.AspNetCore.HttpOverrides.IPNetwork.TryParse(System.ReadOnlySpan<char> networkSpan, out Microsoft.AspNetCore.HttpOverrides.IPNetwork? network) -> bool
Loading