Created
July 20, 2025 13:01
-
-
Save wolfenrain/74fc831ad4f8ffacdb657f0559c5919a to your computer and use it in GitHub Desktop.
CIDR network range interface for Dart
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:io'; | |
import 'dart:typed_data'; | |
import 'package:meta/meta.dart'; | |
/// Represents a range of [InternetAddress] in a network. | |
/// | |
/// {@template cidr} | |
/// Any instance of [CIDR] is lazy iterable of [InternetAddress] and looping | |
/// over it can take a long time as the range of IPs can be astronomically high. | |
/// | |
/// The [contains] method can however be safely used as it is optimized to check | |
/// using the bytes representation of an [InternetAddress]. | |
/// {@endtemplate} | |
/// | |
/// Usage: | |
/// | |
/// ```dart | |
/// // You can create an instance using an InternetAddress directly. | |
/// final cidr4 = CIDR(InternetAddress('192.168.1.0', prefixLength: 24)); | |
/// | |
/// // You can use an instance of InternetAddress or a String to check if it is | |
/// // within the range. | |
/// print(cidr4.contains(InternetAddress('192.168.1.5'))); // true | |
/// print(cidr4.containsString('192.168.2.5')); // false | |
/// | |
/// // You can create an instance by parsing a CIDR notation. | |
/// final cidr6 = CIDR.parse('2001:db8::/126'); | |
/// print(cidr6.containsString('2001:db8::1')); // true | |
/// print(cidr6.contains(InternetAddress('2001:db9::1'))); // false | |
/// ``` | |
@immutable | |
class CIDR extends Iterable<InternetAddress> { | |
/// Create a new [CIDR] instance from an [ipAddress] and the [prefixLength] | |
/// | |
/// {@macro cidr}. | |
const CIDR(this.ipAddress, {required this.prefixLength}); | |
/// Parse a CIDR [notation] string into a [CIDR] instance. | |
/// | |
/// {@macro cidr} | |
factory CIDR.parse(String notation) { | |
final parts = notation.split('/'); | |
if (parts.length != 2) { | |
throw const FormatException('Notation has to be a valid CIDR notation'); | |
} | |
final [address, bytes] = parts; | |
// Try to parse the address. | |
final ipAddress = InternetAddress.tryParse(address); | |
if (ipAddress == null) { | |
throw const FormatException('Ip address must be a valid address'); | |
} | |
// Try to parse the prefix value. | |
final prefixLength = int.tryParse(bytes); | |
if (prefixLength == null) { | |
throw const FormatException('Prefix length must be a decimal value'); | |
} | |
return CIDR(ipAddress, prefixLength: prefixLength); | |
} | |
/// Parse a CIDR [notation] string into a [CIDR] instance. | |
/// | |
/// Like [CIDR.parse] except that this function returns `null` where a | |
/// similar call to [CIDR.parse] would throw a [FormatException]. | |
static CIDR? tryParse(String notation) { | |
try { | |
return CIDR.parse(notation); | |
} on FormatException { | |
return null; | |
} | |
} | |
/// The starting address of the network. | |
/// | |
/// It represents the network itself and serves as the base for the range of | |
/// addresses covered by this [CIDR] instance. | |
final InternetAddress ipAddress; | |
/// The number of significant bits in the [ipAddress] that represent the | |
/// portion of the network. | |
final int prefixLength; | |
/// The number of [InternetAddress]es in this [CIDR]. | |
/// | |
/// Unlike the [length], which gets computed by iterating through each element | |
/// this returns a computed value. | |
BigInt get computedLength { | |
return BigInt.one << ((ipAddress.rawAddress.length * 8) - prefixLength); | |
} | |
/// The number of [InternetAddress]es in this [CIDR]. | |
/// | |
/// This will iterate through all the elements in the iterable and will most | |
/// likely be slow as IP ranges can have many hosts. And the returned value | |
/// might be clamped to the max int value of the platform. | |
/// | |
/// For a correct and fast length value use the [computedLength]. | |
@override | |
int get length => super.length; | |
@override | |
bool operator ==(Object other) { | |
return switch (other) { | |
CIDR(:final ipAddress, :final prefixLength) => | |
this.ipAddress == ipAddress && prefixLength == prefixLength, | |
_ => false, | |
}; | |
} | |
@override | |
int get hashCode => Object.hash(ipAddress, prefixLength); | |
@override | |
Iterator<InternetAddress> get iterator => _CIDRIterator(this); | |
/// Check if [address] is in the range. | |
/// | |
/// The [containsString] call this by converting the string input to a | |
/// [InternetAddress]. | |
@override | |
bool contains(covariant InternetAddress? address) { | |
// If it is not an internet address, then there is no need to keep checking | |
// the rest. | |
if (address is! InternetAddress) { | |
return false; | |
} | |
// If the types are not the same then they are not comparable at all. | |
if (ipAddress.type != address.type) { | |
return false; | |
} | |
// Get the bytes of both addresses. | |
final ipBytes = ipAddress.rawAddress; | |
final otherBytes = address.rawAddress; | |
final prefixBytes = prefixLength ~/ 8; | |
final remainingBits = prefixLength % 8; | |
// Compare the full bytes first. If any is not equal we know they aren't the | |
// same. | |
for (var i = 0; i < prefixBytes; i++) { | |
if (ipBytes[i] != otherBytes[i]) { | |
return false; | |
} | |
} | |
// If there are any remaining bits we can compare the last byte. | |
if (remainingBits > 0) { | |
final mask = 0xFF << (8 - remainingBits) & 0xFF; | |
if ((ipBytes[prefixBytes] & mask) != (otherBytes[prefixBytes] & mask)) { | |
return false; | |
} | |
} | |
return true; | |
} | |
/// Check if [address] is in the range. | |
/// | |
/// See [contains] for the [InternetAddress] implementation. | |
bool containsString(String address) => contains(InternetAddress(address)); | |
/// Copy this instance into a new instance with different [ipAddress] or | |
/// [prefixLength] values. | |
CIDR copyWith({InternetAddress? ipAddress, int? prefixLength}) => CIDR( | |
ipAddress ?? this.ipAddress, | |
prefixLength: prefixLength ?? this.prefixLength, | |
); | |
} | |
class _CIDRIterator implements Iterator<InternetAddress> { | |
_CIDRIterator(CIDR cidr) | |
: _base = _toInt(cidr.ipAddress.rawAddress), | |
_length = cidr.ipAddress.rawAddress.length, | |
_total = cidr.computedLength, | |
_current = BigInt.zero; | |
/// An [InternetAddress]'s raw representation as a big integer. | |
final BigInt _base; | |
/// The total length of bytes in [InternetAddress]'s raw representation. | |
final int _length; | |
/// The total amount of hosts within this range. | |
final BigInt _total; | |
/// The current big int value, which should be added to [_base] to get the | |
/// [current] [InternetAddress]. | |
BigInt _current; | |
@override | |
InternetAddress get current { | |
final hex = (_base + _current).toRadixString(16).padLeft(_length * 2, '0'); | |
return InternetAddress.fromRawAddress( | |
Uint8List.fromList([ | |
for (var i = 0; i < _length; i++) | |
int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16), | |
]), | |
); | |
} | |
@override | |
bool moveNext() { | |
if (_current == _total) return false; | |
_current += BigInt.one; | |
return true; | |
} | |
/// Convert a list of [bytes] to a [BigInt]. | |
static BigInt _toInt(Uint8List bytes) => BigInt.parse( | |
bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(), | |
radix: 16, | |
); | |
} | |
void main() { | |
final cidr = CIDR.parse('10.0.0.0/24'); | |
print(cidr.containsString('10.0.0.2')); // true | |
print(cidr.containsString('10.0.1.2')); // false | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment