Skip to content

Commit e4829c1

Browse files
authored
Feat: parserange (#364)
IP Range to CIDR Converter A C# utility for converting IP address ranges into optimal CIDR blocks, supporting both IPv4 and IPv6 addresses. Methods ParseRange(string range) Parses an IP range string and returns the optimal CIDR blocks. Throws exceptions on invalid input. Parameters: range (string): IP range in format "startIP-endIP" (e.g., "192.168.1.45-192.168.1.65") Returns: IEnumerable<IPNetwork2> - Collection of optimal CIDR blocks Throws: ArgumentException, FormatException for invalid input TryParseRange(string range, out IEnumerable<IPNetwork2> result) Attempts to parse an IP range string. Returns success/failure without throwing exceptions. Parameters: range (string): IP range in format "startIP-endIP" result (out): Collection of CIDR blocks if successful, null if failed Returns: bool - True if parsing succeeded, false otherwise * Feat: parserange * Chore: Test units * Chore: upgrade version * Chore: documentation * Fix: sonarqube issues * Fix: formatting * Fix: 100% coverage
1 parent 0d02478 commit e4829c1

File tree

7 files changed

+792
-38
lines changed

7 files changed

+792
-38
lines changed

README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,57 @@ var maxSubnet = UniqueLocalAddress.CreateUlaSubnet(randomUla, UniqueLocalAddress
498498
Console.WriteLine($"Subnet 1: {subnet1}"); // e.g., fd12:3456:
499499
```
500500

501+
---
502+
## ParseRange and TryParseRange
503+
504+
A C# utility for converting IP address ranges into optimal CIDR blocks, supporting both IPv4 and IPv6 addresses.
505+
Both IPv4 and IPv6 ranges are supported
506+
The algorithm generates the minimal set of CIDR blocks that exactly cover the specified range
507+
Input format must be "startIP-endIP" with a single hyphen separator
508+
Whitespace around IP addresses is automatically trimmed
509+
Mixed IPv4/IPv6 ranges are not supported (both addresses must be the same family)
510+
511+
### IPv4 Example
512+
513+
```C#
514+
string ipv4Range = "192.168.1.45 - 192.168.1.65";
515+
var ipv4Blocks = IPNetwork2.ParseRange(ipv4Range);
516+
517+
Console.WriteLine($"CIDR blocks for {ipv4Range}:");
518+
foreach (var block in ipv4Blocks)
519+
{
520+
Console.WriteLine($" {block}");
521+
}
522+
```
523+
524+
Output
525+
```
526+
CIDR blocks for 192.168.1.45 - 192.168.1.65:
527+
192.168.1.45/32
528+
192.168.1.46/31
529+
192.168.1.48/28
530+
192.168.1.64/31
531+
```
532+
533+
### IPv6 Example
534+
535+
```C#
536+
string ipv6Range = "2001:db8::1000 - 2001:db8::1fff";
537+
var ipv6Blocks = IPNetwork2.ParseRange(ipv6Range);
538+
539+
Console.WriteLine($"CIDR blocks for {ipv6Range}:");
540+
foreach (var block in ipv6Blocks)
541+
{
542+
Console.WriteLine($" {block}");
543+
}
544+
```
545+
546+
Ouput
547+
````
548+
CIDR blocks for 2001:db8::1000 - 2001:db8::1fff:
549+
2001:db8::1000/116
550+
````
551+
501552
---
502553

503554
## IPNetwork utility command line

appveyor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
version: '3.3.{build}'
1+
version: '3.4.{build}'
22
image: Visual Studio 2022
33

44
assembly_info:

src/System.Net.IPNetwork/IPNetwork2InternalParse.cs

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -110,17 +110,6 @@ private static bool InternalParse(bool tryParse, string network, ICidrGuess cidr
110110

111111
StringSplitOptions splitOptions = sanitize ? StringSplitOptions.RemoveEmptyEntries : StringSplitOptions.None;
112112
string[] args = network.Split([' ', '/'], splitOptions);
113-
114-
if (args.Length == 0)
115-
{
116-
if (!tryParse)
117-
{
118-
throw new ArgumentNullException(nameof(network));
119-
}
120-
121-
ipnetwork = null;
122-
return false;
123-
}
124113

125114
if (args.Length == 1)
126115
{
@@ -151,15 +140,14 @@ private static bool InternalParse(bool tryParse, string network, ICidrGuess cidr
151140
bool parsed3 = InternalParse(tryParse, args[0], args[1], out ipnetwork);
152141
return parsed3;
153142
}
154-
else
143+
144+
if (!tryParse)
155145
{
156-
if (!tryParse)
157-
{
158-
throw new ArgumentNullException(nameof(network));
159-
}
160-
ipnetwork = null;
161-
return false;
146+
throw new ArgumentNullException(nameof(network));
162147
}
148+
149+
ipnetwork = null;
150+
return false;
163151
}
164152

165153
/// <summary>
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
// <copyright file="IPNetwork2Parse.cs" company="IPNetwork">
2+
// Copyright (c) IPNetwork. All rights reserved.
3+
// </copyright>
4+
5+
using System.Collections.Generic;
6+
using System.Net.Sockets;
7+
using System.Numerics;
8+
9+
namespace System.Net;
10+
11+
/// <summary>
12+
/// the parse methods.
13+
/// </summary>
14+
public partial class IPNetwork2
15+
{
16+
/// <summary>
17+
/// 192.168.1.45 - 192.168.1.65
18+
///
19+
/// ```
20+
/// 192.168.1.45/32 (covers: 192.168.1.45)
21+
/// 192.168.1.46/31 (covers: 192.168.1.46 - 192.168.1.47)
22+
/// 192.168.1.48/28 (covers: 192.168.1.48 - 192.168.1.63)
23+
/// 192.168.1.64/31 (covers: 192.168.1.64 - 192.168.1.65)
24+
/// ```
25+
///
26+
/// </summary>
27+
/// <param name="range">A string containing an ip range to convert (192.168.1.45 - 192.168.1.65).</param>
28+
/// <param name="ipnetworks">An IPNetwork List equivalent to the network contained in the range</param>
29+
/// <returns>true if parse was successful, false if the parse failed.</returns>
30+
public static bool TryParseRange(string range, out IEnumerable<IPNetwork2> ipnetworks)
31+
{
32+
return InternalParseRange(true, range, out ipnetworks);
33+
}
34+
35+
/// <summary>
36+
/// 192.168.1.45 - 192.168.1.65
37+
///
38+
/// ```
39+
/// 192.168.1.45/32 (covers: 192.168.1.45)
40+
/// 192.168.1.46/31 (covers: 192.168.1.46 - 192.168.1.47)
41+
/// 192.168.1.48/28 (covers: 192.168.1.48 - 192.168.1.63)
42+
/// 192.168.1.64/31 (covers: 192.168.1.64 - 192.168.1.65)
43+
/// ```
44+
///
45+
/// </summary>
46+
/// <param name="start">A string containing an ip range start (**192.168.1.45** - 192.168.1.65).</param>
47+
/// <param name="end">A string containing an ip range end (192.168.1.45 - **192.168.1.65**).</param>
48+
/// <param name="ipnetworks">An IPNetwork List equivalent to the network contained in the range</param>
49+
/// <returns>true if parse was successful, false if the parse failed.</returns>
50+
public static bool TryParseRange(string start, string end, out IEnumerable<IPNetwork2> ipnetworks)
51+
{
52+
return InternalParseRange(true, start, end, out ipnetworks);
53+
}
54+
55+
/// <summary>
56+
/// 192.168.1.45 - 192.168.1.65
57+
///
58+
/// ```
59+
/// 192.168.1.45/32 (covers: 192.168.1.45)
60+
/// 192.168.1.46/31 (covers: 192.168.1.46 - 192.168.1.47)
61+
/// 192.168.1.48/28 (covers: 192.168.1.48 - 192.168.1.63)
62+
/// 192.168.1.64/31 (covers: 192.168.1.64 - 192.168.1.65)
63+
/// ```
64+
///
65+
/// </summary>
66+
/// <param name="range">A string containing an ip range to convert (192.168.1.45 - 192.168.1.65).</param>
67+
/// <returns>An IPNetwork List equivalent to the network contained in the range.</returns>
68+
public static IEnumerable<IPNetwork2> ParseRange(string range)
69+
{
70+
InternalParseRange(false, range, out IEnumerable<IPNetwork2> ipnetworks);
71+
return ipnetworks;
72+
}
73+
74+
/// <summary>
75+
/// 192.168.1.45, 192.168.1.65
76+
///
77+
/// ```
78+
/// 192.168.1.45/32 (covers: 192.168.1.45)
79+
/// 192.168.1.46/31 (covers: 192.168.1.46 - 192.168.1.47)
80+
/// 192.168.1.48/28 (covers: 192.168.1.48 - 192.168.1.63)
81+
/// 192.168.1.64/31 (covers: 192.168.1.64 - 192.168.1.65)
82+
/// ```
83+
/// </summary>
84+
/// <param name="start">A string containing a start range ip address.</param>
85+
/// <param name="end">A string containing a end range ip address.</param>
86+
/// <returns>An IPNetwork List equivalent to the network contained in the range.</returns>
87+
public static IEnumerable<IPNetwork2> ParseRange(string start, string end)
88+
{
89+
InternalParseRange(false, start, end, out IEnumerable<IPNetwork2> ipnetworks);
90+
return ipnetworks;
91+
}
92+
93+
/// <summary>
94+
/// 192.168.1.45 - 192.168.1.65
95+
///
96+
/// ```
97+
/// 192.168.1.45/32 (covers: 192.168.1.45)
98+
/// 192.168.1.46/31 (covers: 192.168.1.46 - 192.168.1.47)
99+
/// 192.168.1.48/28 (covers: 192.168.1.48 - 192.168.1.63)
100+
/// 192.168.1.64/31 (covers: 192.168.1.64 - 192.168.1.65)
101+
/// ```
102+
/// </summary>
103+
/// <param name="tryParse">Whether to throw exception or not during conversion.</param>
104+
/// <param name="start">A string containing a start range ip address.</param>
105+
/// <param name="end">A string containing a end range ip address.</param>
106+
/// <param name="ipnetworks">The resulting IPNetworks.</param>
107+
internal static bool InternalParseRange(bool tryParse, string start, string end, out IEnumerable<IPNetwork2> ipnetworks)
108+
{
109+
bool startParsed = IPAddress.TryParse(start, out IPAddress startIp);
110+
if (!startParsed)
111+
{
112+
if (!tryParse)
113+
{
114+
throw new ArgumentException("Invalid start IPAddress", nameof(start));
115+
}
116+
117+
ipnetworks = null;
118+
return false;
119+
}
120+
121+
bool endParsed = IPAddress.TryParse(end, out IPAddress endIp);
122+
if (!endParsed)
123+
{
124+
if (!tryParse)
125+
{
126+
throw new ArgumentException("Invalid end IPAddress", nameof(end));
127+
}
128+
129+
ipnetworks = null;
130+
return false;
131+
}
132+
133+
bool parsed = InternalParseRange(tryParse, startIp, endIp, out ipnetworks);
134+
return parsed;
135+
}
136+
137+
/// <summary>
138+
/// Internal parse an IPNetwork2.
139+
/// </summary>
140+
/// <param name="tryParse">Prevent exception.</param>
141+
/// <param name="range">The network range parse.</param>
142+
/// <param name="ipnetworks">The resulting IPNetworks.</param>
143+
/// <exception cref="ArgumentNullException">When network is null.</exception>
144+
/// <exception cref="ArgumentException">When network is not valid.</exception>
145+
/// <returns>true if parsed, otherwise false</returns>
146+
internal static bool InternalParseRange(bool tryParse, string range, out IEnumerable<IPNetwork2> ipnetworks)
147+
{
148+
if (string.IsNullOrEmpty(range))
149+
{
150+
if (!tryParse)
151+
{
152+
throw new ArgumentNullException(nameof(range));
153+
}
154+
155+
ipnetworks = null;
156+
return false;
157+
}
158+
159+
string[] args = range.Split([' ', '-'], StringSplitOptions.RemoveEmptyEntries);
160+
if (args.Length == 2)
161+
{
162+
bool parsed3 = InternalParseRange(tryParse, args[0], args[1], out ipnetworks);
163+
return parsed3;
164+
}
165+
166+
if (!tryParse)
167+
{
168+
throw new ArgumentOutOfRangeException(nameof(range));
169+
}
170+
ipnetworks = null;
171+
return false;
172+
}
173+
174+
/// <summary>
175+
/// 192.168.168.100 255.255.255.0
176+
///
177+
/// Network : 192.168.168.0
178+
/// Netmask : 255.255.255.0
179+
/// Cidr : 24
180+
/// Start : 192.168.168.1
181+
/// End : 192.168.168.254
182+
/// Broadcast : 192.168.168.255.
183+
/// </summary>
184+
/// <param name="tryParse">Whether to throw exception or not during conversion.</param>
185+
/// <param name="start">A start range ip address.</param>
186+
/// <param name="end">An end range ip address.</param>
187+
/// <param name="ipnetworks">The resulting IPNetworks.</param>
188+
internal static bool InternalParseRange(bool tryParse, IPAddress start, IPAddress end, out IEnumerable<IPNetwork2> ipnetworks)
189+
{
190+
if (start == null)
191+
{
192+
if (!tryParse)
193+
{
194+
throw new ArgumentNullException(nameof(start));
195+
}
196+
197+
ipnetworks = null;
198+
return false;
199+
}
200+
201+
if (end == null)
202+
{
203+
if (!tryParse)
204+
{
205+
throw new ArgumentNullException(nameof(end));
206+
}
207+
ipnetworks = null;
208+
return false;
209+
}
210+
211+
if (end.AddressFamily != start.AddressFamily)
212+
{
213+
if (!tryParse)
214+
{
215+
throw new ArgumentException(nameof(AddressFamily));
216+
}
217+
ipnetworks = null;
218+
return false;
219+
}
220+
221+
var result = new List<IPNetwork2>();
222+
223+
var startValue = ToBigInteger(start);
224+
var endValue = ToBigInteger(end);
225+
226+
if (startValue > endValue)
227+
{
228+
throw new ArgumentException("Start IP must be less than or equal to end IP", nameof(end));
229+
}
230+
231+
var addressFamily = start.AddressFamily;
232+
byte addressBits = addressFamily == AddressFamily.InterNetworkV6 ? (byte)128 : (byte)32;
233+
234+
var current = startValue;
235+
while (current <= endValue)
236+
{
237+
// Find the largest CIDR block that starts at current and doesn't exceed endValue
238+
byte prefixLength = FindOptimalPrefixLength(current, endValue, addressBits);
239+
240+
var network = new IPNetwork2(current, addressFamily, prefixLength);
241+
result.Add(network);
242+
243+
// Move to the next IP after this block
244+
uint blockSize = (uint)(1 << (addressBits - prefixLength));
245+
current += blockSize;
246+
}
247+
248+
ipnetworks = result;
249+
return true;
250+
}
251+
252+
private static byte FindOptimalPrefixLength(BigInteger startIp, BigInteger endIp, int addressBits)
253+
{
254+
BigInteger remainingIps = endIp - startIp + 1;
255+
256+
// Find the number of trailing zeros in startIp (alignment)
257+
int alignment = startIp.IsZero ? addressBits : CountTrailingZeros(startIp);
258+
259+
// Find the largest power of 2 that fits in the remaining range
260+
int maxBlockSizeBits = remainingIps.IsZero ? 0 : GetHighestBitPosition(remainingIps);
261+
262+
// Take the minimum of alignment and what fits in range
263+
int blockSizeBits = Math.Min(alignment, maxBlockSizeBits);
264+
265+
// Convert to prefix length
266+
return (byte)(addressBits - blockSizeBits);
267+
}
268+
269+
private static int CountTrailingZeros(BigInteger value)
270+
{
271+
if (value.IsZero) return 0;
272+
273+
int count = 0;
274+
while ((value & BigInteger.One) == 0)
275+
{
276+
value >>= 1;
277+
count++;
278+
}
279+
return count;
280+
}
281+
282+
private static int GetHighestBitPosition(BigInteger value)
283+
{
284+
if (value.IsZero) return 0;
285+
286+
int position = 0;
287+
while (value > 1)
288+
{
289+
value >>= 1;
290+
position++;
291+
}
292+
return position;
293+
}
294+
}

0 commit comments

Comments
 (0)