Skip to content

Commit 312706f

Browse files
authored
Add IPNetwork.Parse and TryParse (#44573)
1 parent 7011809 commit 312706f

File tree

3 files changed

+268
-8
lines changed

3 files changed

+268
-8
lines changed

src/Middleware/HttpOverrides/src/IPNetwork.cs

Lines changed: 111 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics.CodeAnalysis;
45
using System.Net;
56
using System.Net.Sockets;
67

@@ -16,9 +17,18 @@ public class IPNetwork
1617
/// </summary>
1718
/// <param name="prefix">The <see cref="IPAddress"/>.</param>
1819
/// <param name="prefixLength">The prefix length.</param>
19-
public IPNetwork(IPAddress prefix, int prefixLength)
20+
/// <exception cref="ArgumentOutOfRangeException"><paramref name="prefixLength"/> is out of range.</exception>
21+
public IPNetwork(IPAddress prefix, int prefixLength) : this(prefix, prefixLength, true)
2022
{
21-
CheckPrefixLengthRange(prefix, prefixLength);
23+
}
24+
25+
private IPNetwork(IPAddress prefix, int prefixLength, bool checkPrefixLengthRange)
26+
{
27+
if (checkPrefixLengthRange &&
28+
!IsValidPrefixLengthRange(prefix, prefixLength))
29+
{
30+
throw new ArgumentOutOfRangeException(nameof(prefixLength), "The prefix length was out of range.");
31+
}
2232

2333
Prefix = prefix;
2434
PrefixLength = prefixLength;
@@ -83,21 +93,114 @@ private byte[] CreateMask()
8393
return mask;
8494
}
8595

86-
private static void CheckPrefixLengthRange(IPAddress prefix, int prefixLength)
96+
private static bool IsValidPrefixLengthRange(IPAddress prefix, int prefixLength)
8797
{
8898
if (prefixLength < 0)
8999
{
90-
throw new ArgumentOutOfRangeException(nameof(prefixLength));
100+
return false;
91101
}
92102

93-
if (prefix.AddressFamily == AddressFamily.InterNetwork && prefixLength > 32)
103+
return prefix.AddressFamily switch
104+
{
105+
AddressFamily.InterNetwork => prefixLength <= 32,
106+
AddressFamily.InterNetworkV6 => prefixLength <= 128,
107+
_ => true
108+
};
109+
}
110+
111+
/// <summary>
112+
/// Converts the specified <see cref="ReadOnlySpan{T}"/> of <see langword="char"/> representation of
113+
/// an IP address and a prefix length to its <see cref="IPNetwork"/> equivalent.
114+
/// </summary>
115+
/// <param name="networkSpan">The <see cref="ReadOnlySpan{T}"/> of <see langword="char"/> to convert, in CIDR notation.</param>
116+
/// <returns>
117+
///The <see cref="IPNetwork"/> equivalent to the IP address and prefix length contained in <paramref name="networkSpan"/>.
118+
/// </returns>
119+
/// <exception cref="FormatException"><paramref name="networkSpan"/> is not in the correct format.</exception>
120+
/// <exception cref="ArgumentOutOfRangeException">The prefix length contained in <paramref name="networkSpan"/> is out of range.</exception>
121+
/// <inheritdoc cref="TryParseComponents(ReadOnlySpan{char}, out IPAddress?, out int)"/>
122+
public static IPNetwork Parse(ReadOnlySpan<char> networkSpan)
123+
{
124+
if (!TryParseComponents(networkSpan, out var prefix, out var prefixLength))
94125
{
95-
throw new ArgumentOutOfRangeException(nameof(prefixLength));
126+
throw new FormatException("An invalid IP address or prefix length was specified.");
96127
}
97128

98-
if (prefix.AddressFamily == AddressFamily.InterNetworkV6 && prefixLength > 128)
129+
if (!IsValidPrefixLengthRange(prefix, prefixLength))
99130
{
100-
throw new ArgumentOutOfRangeException(nameof(prefixLength));
131+
throw new ArgumentOutOfRangeException(nameof(networkSpan), "The prefix length was out of range.");
101132
}
133+
134+
return new IPNetwork(prefix, prefixLength, false);
135+
}
136+
137+
/// <summary>
138+
/// Converts the specified <see cref="ReadOnlySpan{T}"/> of <see langword="char"/> representation of
139+
/// an IP address and a prefix length to its <see cref="IPNetwork"/> equivalent, and returns a value
140+
/// that indicates whether the conversion succeeded.
141+
/// </summary>
142+
/// <param name="networkSpan">The <see cref="ReadOnlySpan{T}"/> of <see langword="char"/> to validate.</param>
143+
/// <param name="network">
144+
/// When this method returns, contains the <see cref="IPNetwork"/> equivalent to the IP Address
145+
/// and prefix length contained in <paramref name="networkSpan"/>, if the conversion succeeded,
146+
/// or <see langword="null"/> if the conversion failed. This parameter is passed uninitialized.
147+
/// </param>
148+
/// <returns>
149+
/// <see langword="true"/> if the <paramref name="networkSpan"/> parameter was
150+
/// converted successfully; otherwise <see langword="false"/>.
151+
/// </returns>
152+
/// <inheritdoc cref="TryParseComponents(ReadOnlySpan{char}, out IPAddress?, out int)"/>
153+
public static bool TryParse(ReadOnlySpan<char> networkSpan, [NotNullWhen(true)] out IPNetwork? network)
154+
{
155+
network = null;
156+
157+
if (!TryParseComponents(networkSpan, out var prefix, out var prefixLength))
158+
{
159+
return false;
160+
}
161+
162+
if (!IsValidPrefixLengthRange(prefix, prefixLength))
163+
{
164+
return false;
165+
}
166+
167+
network = new IPNetwork(prefix, prefixLength, false);
168+
return true;
169+
}
170+
171+
/// <remarks>
172+
/// <para>
173+
/// The specified representation must be expressed using CIDR (Classless Inter-Domain Routing) notation, or 'slash notation',
174+
/// which contains an IPv4 or IPv6 address and the subnet mask prefix length, separated by a forward slash.
175+
/// </para>
176+
/// <example>
177+
/// e.g. <c>"192.168.0.1/31"</c> for IPv4, <c>"2001:db8:3c4d::1/127"</c> for IPv6
178+
/// </example>
179+
/// </remarks>
180+
private static bool TryParseComponents(
181+
ReadOnlySpan<char> networkSpan,
182+
[NotNullWhen(true)] out IPAddress? prefix,
183+
out int prefixLength)
184+
{
185+
prefix = null;
186+
prefixLength = default;
187+
188+
var forwardSlashIndex = networkSpan.IndexOf('/');
189+
if (forwardSlashIndex < 0)
190+
{
191+
return false;
192+
}
193+
194+
if (!IPAddress.TryParse(networkSpan.Slice(0, forwardSlashIndex), out prefix))
195+
{
196+
return false;
197+
}
198+
199+
if (!int.TryParse(networkSpan.Slice(forwardSlashIndex + 1), out prefixLength))
200+
{
201+
return false;
202+
}
203+
204+
return true;
102205
}
103206
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
#nullable enable
2+
static Microsoft.AspNetCore.HttpOverrides.IPNetwork.Parse(System.ReadOnlySpan<char> networkSpan) -> Microsoft.AspNetCore.HttpOverrides.IPNetwork!
3+
static Microsoft.AspNetCore.HttpOverrides.IPNetwork.TryParse(System.ReadOnlySpan<char> networkSpan, out Microsoft.AspNetCore.HttpOverrides.IPNetwork? network) -> bool

src/Middleware/HttpOverrides/test/IPNetworkTest.cs

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,159 @@ public void Contains_Negative(string prefixText, int length, string addressText)
3939
var network = new IPNetwork(IPAddress.Parse(prefixText), length);
4040
Assert.False(network.Contains(IPAddress.Parse(addressText)));
4141
}
42+
43+
[Theory]
44+
[InlineData("192.168.1.1", 0)]
45+
[InlineData("192.168.1.1", 32)]
46+
[InlineData("2001:db8:3c4d::1", 0)]
47+
[InlineData("2001:db8:3c4d::1", 128)]
48+
public void Ctor_WithValidFormat_IsSuccessfullyCreated(string prefixText, int prefixLength)
49+
{
50+
// Arrange
51+
var address = IPAddress.Parse(prefixText);
52+
53+
// Act
54+
var network = new IPNetwork(address, prefixLength);
55+
56+
// Assert
57+
Assert.Equal(prefixText, network.Prefix.ToString());
58+
Assert.Equal(prefixLength, network.PrefixLength);
59+
}
60+
61+
[Theory]
62+
[InlineData("192.168.1.1", -1)]
63+
[InlineData("192.168.1.1", 33)]
64+
[InlineData("2001:db8:3c4d::1", -1)]
65+
[InlineData("2001:db8:3c4d::1", 129)]
66+
public void Ctor_WithPrefixLengthOutOfRange_ThrowsArgumentOutOfRangeException(string prefixText, int prefixLength)
67+
{
68+
// Arrange
69+
var address = IPAddress.Parse(prefixText);
70+
71+
// Act
72+
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new IPNetwork(address, prefixLength));
73+
74+
// Assert
75+
Assert.StartsWith("The prefix length was out of range.", ex.Message);
76+
}
77+
78+
[Theory]
79+
[MemberData(nameof(ValidPrefixWithPrefixLengthData))]
80+
public void Parse_WithValidFormat_ParsedCorrectly(string input, string expectedPrefix, int expectedPrefixLength)
81+
{
82+
// Act
83+
var network = IPNetwork.Parse(input);
84+
85+
// Assert
86+
Assert.Equal(expectedPrefix, network.Prefix.ToString());
87+
Assert.Equal(expectedPrefixLength, network.PrefixLength);
88+
}
89+
90+
[Theory]
91+
[InlineData(null)]
92+
[MemberData(nameof(InvalidPrefixOrPrefixLengthData))]
93+
public void Parse_WithInvalidFormat_ThrowsFormatException(string input)
94+
{
95+
// Arrange & Act & Assert
96+
var ex = Assert.Throws<FormatException>(() => IPNetwork.Parse(input));
97+
Assert.Equal("An invalid IP address or prefix length was specified.", ex.Message);
98+
}
99+
100+
[Theory]
101+
[MemberData(nameof(PrefixLengthOutOfRangeData))]
102+
public void Parse_WithOutOfRangePrefixLength_ThrowsArgumentOutOfRangeException(string input)
103+
{
104+
// Arrange & Act & Assert
105+
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => IPNetwork.Parse(input));
106+
Assert.StartsWith("The prefix length was out of range.", ex.Message);
107+
}
108+
109+
[Theory]
110+
[MemberData(nameof(ValidPrefixWithPrefixLengthData))]
111+
public void TryParse_WithValidFormat_ParsedCorrectly(string input, string expectedPrefix, int expectedPrefixLength)
112+
{
113+
// Act
114+
var result = IPNetwork.TryParse(input, out var network);
115+
116+
// Assert
117+
Assert.True(result);
118+
Assert.NotNull(network);
119+
Assert.Equal(expectedPrefix, network.Prefix.ToString());
120+
Assert.Equal(expectedPrefixLength, network.PrefixLength);
121+
}
122+
123+
[Theory]
124+
[InlineData(null)]
125+
[MemberData(nameof(InvalidPrefixOrPrefixLengthData))]
126+
[MemberData(nameof(PrefixLengthOutOfRangeData))]
127+
public void TryParse_WithInvalidFormat_ReturnsFalse(string input)
128+
{
129+
// Act
130+
var result = IPNetwork.TryParse(input, out var network);
131+
132+
// Assert
133+
Assert.False(result);
134+
Assert.Null(network);
135+
}
136+
137+
public static TheoryData<string, string, int> ValidPrefixWithPrefixLengthData() => new()
138+
{
139+
// IPv4
140+
{ "10.1.0.0/16", "10.1.0.0", 16 },
141+
{ "10.1.1.0/8", "10.1.1.0", 8 },
142+
{ "174.0.0.0/7", "174.0.0.0", 7 },
143+
{ "10.174.0.0/15", "10.174.0.0", 15 },
144+
{ "10.168.0.0/14", "10.168.0.0", 14 },
145+
{ "192.168.0.1/31", "192.168.0.1", 31 },
146+
{ "192.168.0.1/31", "192.168.0.1", 31 },
147+
{ "192.168.0.1/32", "192.168.0.1", 32 },
148+
{ "192.168.1.1/0", "192.168.1.1", 0 },
149+
{ "192.168.1.1/0", "192.168.1.1", 0 },
150+
151+
// IPv6
152+
{ "2001:db8:3c4d::/127", "2001:db8:3c4d::", 127 },
153+
{ "2001:db8:3c4d::1/128", "2001:db8:3c4d::1", 128 },
154+
{ "2001:db8:3c4d::1/0", "2001:db8:3c4d::1", 0 },
155+
{ "2001:db8:3c4d::1/0", "2001:db8:3c4d::1", 0 }
156+
};
157+
158+
public static TheoryData<string> InvalidPrefixOrPrefixLengthData() => new()
159+
{
160+
string.Empty,
161+
"abcdefg",
162+
163+
// Missing forward slash
164+
"10.1.0.016",
165+
"2001:db8:3c4d::1127",
166+
167+
// Invalid prefix
168+
"/16",
169+
"10.1./16",
170+
"10.1.0./16",
171+
"10.1.ABC.0/16",
172+
"200123:db8:3c4d::/127",
173+
":db8:3c4d::/127",
174+
"2001:?:3c4d::1/0",
175+
176+
// Invalid prefix length
177+
"10.1.0.0/",
178+
"10.1.0.0/16-",
179+
"10.1.0.0/ABC",
180+
"2001:db8:3c4d::/",
181+
"2001:db8:3c4d::1/128-",
182+
"2001:db8:3c4d::1/ABC"
183+
};
184+
185+
public static TheoryData<string> PrefixLengthOutOfRangeData() => new()
186+
{
187+
// Negative prefix length
188+
"10.1.0.0/-16",
189+
"2001:db8:3c4d::/-127",
190+
191+
// Prefix length out of range (IPv4)
192+
"10.1.0.0/33",
193+
194+
// Prefix length out of range (IPv6)
195+
"2001:db8:3c4d::/129"
196+
};
42197
}

0 commit comments

Comments
 (0)