// ip.mbt — IPv4/IPv6 parsing and CIDR matching for `ipMatch`.
//
// Implements the subset of Go's `net` package that Casbin's `ipMatch`
// relies on: `ParseIP` (IPv4; IPv6 with `::` compression and an
// IPv4-embedded tail) and `ParseCIDR` prefix containment. Addresses are
// kept as byte sequences (4 for IPv4, 16 for IPv6), so addresses of
// different families are simply unequal — the same outcome as Go, where
// `cidr.Contains` returns false across families.
//
// Deliberate deviations from Go: IPv4 octets with leading zeros are
// rejected (Go rejects them since 1.17), and zone identifiers such as
// `fe80::1%eth0` are not supported.
///|
/// Whether `ip1` is covered by `ip2`, where `ip2` is either a CIDR
/// (`192.168.2.0/24`) or a plain address.
///
/// Raises `MatcherEval` when either argument does not parse, mirroring the
/// errors Casbin's `ipMatch` raises.
pub fn ip_match(ip1 : String, ip2 : String) -> Bool raise CasbinError {
let address = match parse_ip(ip1) {
Some(address) => address
None =>
raise casbin_error(
MatcherEval,
"ipMatch: \"" + ip1 + "\" is not an IP address",
)
}
match parse_cidr(ip2) {
Some((network, prefix)) =>
network.length() == address.length() &&
prefix_matches(address, network, prefix)
None =>
match parse_ip(ip2) {
Some(other) =>
other.length() == address.length() &&
prefix_matches(address, other, address.length() * 8)
None =>
raise casbin_error(
MatcherEval,
"ipMatch: \"" + ip2 + "\" is neither an IP address nor a CIDR",
)
}
}
}
///|
/// Parses an IPv4 or IPv6 address into 4 or 16 bytes.
fn parse_ip(text : String) -> Array[Int]? {
if text.contains(":") {
parse_ipv6(text)
} else {
parse_ipv4(text)
}
}
///|
fn parse_ipv4(text : String) -> Array[Int]? {
let parts = text.split(".").collect()
if parts.length() != 4 {
return None
}
let bytes : Array[Int] = []
for part in parts {
match parse_octet(part) {
Some(value) => bytes.push(value)
None => return None
}
}
Some(bytes)
}
///|
fn parse_octet(part : StringView) -> Int? {
let chars : Array[Char] = part.iter().collect()
if chars.length() == 0 || chars.length() > 3 {
return None
}
if chars.length() > 1 && chars[0] == '0' {
return None
}
let mut value = 0
for ch in chars {
if !ch.is_ascii_digit() {
return None
}
value = value * 10 + (ch.to_int() - '0'.to_int())
}
if value > 255 {
return None
}
Some(value)
}
///|
/// Parses an IPv6 address (`::` compression and an IPv4 tail are
/// supported) into 16 bytes.
fn parse_ipv6(text : String) -> Array[Int]? {
let parts = text.split("::").collect()
if parts.length() > 2 {
return None
}
let mut groups : Array[Int] = []
match parse_ipv6_side(parts[0]) {
Some(left) => groups = left
None => return None
}
if parts.length() == 2 {
let right = match parse_ipv6_side(parts[1]) {
Some(right) => right
None => return None
}
let total = groups.length() + right.length()
// `::` must stand for at least one zero group.
if total > 7 {
return None
}
for _i in 0..<(8 - total) {
groups.push(0)
}
for group in right {
groups.push(group)
}
} else if groups.length() != 8 {
return None
}
let bytes : Array[Int] = []
for group in groups {
bytes.push(group / 256)
bytes.push(group % 256)
}
Some(bytes)
}
///|
/// Parses one side of an IPv6 address into 16-bit groups; an IPv4 tail
/// contributes two groups.
fn parse_ipv6_side(part : StringView) -> Array[Int]? {
if part.is_empty() {
return Some([])
}
let fields = part.split(":").collect()
let groups : Array[Int] = []
for i in 0.. tail
None => return None
}
groups.push(tail[0] * 256 + tail[1])
groups.push(tail[2] * 256 + tail[3])
} else {
match parse_hex_group(field) {
Some(value) => groups.push(value)
None => return None
}
}
}
Some(groups)
}
///|
fn parse_hex_group(field : StringView) -> Int? {
let chars : Array[Char] = field.iter().collect()
if chars.length() == 0 || chars.length() > 4 {
return None
}
let mut value = 0
for ch in chars {
let digit = if ch.is_ascii_digit() {
ch.to_int() - '0'.to_int()
} else if ch >= 'a' && ch <= 'f' {
ch.to_int() - 'a'.to_int() + 10
} else if ch >= 'A' && ch <= 'F' {
ch.to_int() - 'A'.to_int() + 10
} else {
return None
}
value = value * 16 + digit
}
Some(value)
}
///|
/// Parses `/`; the prefix must fit the address family.
fn parse_cidr(text : String) -> (Array[Int], Int)? {
let parts = text.split("/").collect()
if parts.length() != 2 {
return None
}
let address = match parse_ip(parts[0].to_owned()) {
Some(address) => address
None => return None
}
let prefix = match parse_decimal(parts[1]) {
Some(prefix) => prefix
None => return None
}
if prefix > address.length() * 8 {
return None
}
Some((address, prefix))
}
///|
fn parse_decimal(text : StringView) -> Int? {
let chars : Array[Char] = text.iter().collect()
if chars.length() == 0 || chars.length() > 3 {
return None
}
let mut value = 0
for ch in chars {
if !ch.is_ascii_digit() {
return None
}
value = value * 10 + (ch.to_int() - '0'.to_int())
}
Some(value)
}
///|
/// Whether the first `prefix` bits of two same-length addresses agree.
fn prefix_matches(
address : Array[Int],
network : Array[Int],
prefix : Int,
) -> Bool {
let full_bytes = prefix / 8
let remaining_bits = prefix % 8
for i in 0.. 0 {
let mask = 0xFF << (8 - remaining_bits)
if (address[full_bytes] & mask) != (network[full_bytes] & mask) {
return false
}
}
true
}