use super::{Anchor, ByteSet, Finder, Memo, RunClass, RunRule};
use std::ops::Range;
#[derive(Default)]
pub struct Cidr {}
impl Cidr {
fn try_ipv4_cidr(input: &[u8], idx: usize) -> Option<Range<usize>> {
if !input[idx].is_ascii_digit() {
return None;
}
if idx > 0
&& (input[idx - 1].is_ascii_digit() || input[idx - 1] == b'.' || input[idx - 1] == b':')
{
return None;
}
let start = idx;
let mut pos = idx;
for octet_idx in 0..4 {
if octet_idx > 0 {
if pos >= input.len() || input[pos] != b'.' {
return None;
}
pos += 1;
}
let octet_start = pos;
while pos < input.len() && input[pos].is_ascii_digit() {
pos += 1;
}
let octet_len = pos - octet_start;
if octet_len == 0 || octet_len > 3 {
return None;
}
if octet_len > 1 && input[octet_start] == b'0' {
return None;
}
let mut value: u16 = 0;
for &b in &input[octet_start..pos] {
value = value * 10 + (b - b'0') as u16;
}
if value > 255 {
return None;
}
}
if pos >= input.len() || input[pos] != b'/' {
return None;
}
pos += 1;
let prefix_start = pos;
while pos < input.len() && input[pos].is_ascii_digit() {
pos += 1;
}
let prefix_len = pos - prefix_start;
if prefix_len == 0 || prefix_len > 2 {
return None;
}
if prefix_len > 1 && input[prefix_start] == b'0' {
return None;
}
let mut prefix: u8 = 0;
for &b in &input[prefix_start..pos] {
prefix = prefix * 10 + (b - b'0');
}
if prefix > 32 {
return None;
}
if pos < input.len() {
if input[pos].is_ascii_digit() {
return None;
}
if input[pos] == b'.' && pos + 1 < input.len() && input[pos + 1].is_ascii_digit() {
return None;
}
}
Some(start..pos)
}
fn try_ipv6_cidr(input: &[u8], idx: usize, memo: &mut Memo) -> Option<Range<usize>> {
let (ip_start, ip_end) = if input[idx] == b'[' {
let window = &input[idx..input.len().min(idx + 47)];
let close = window.iter().position(|&b| b == b']')?;
let close_pos = idx + close;
let inner = &input[idx + 1..close_pos];
if !crate::ipv6::is_valid_ipv6(inner) {
return None;
}
(idx, close_pos + 1)
} else {
if !input[idx].is_ascii_hexdigit() && input[idx] != b':' {
return None;
}
if idx > 0 && (input[idx - 1].is_ascii_alphanumeric() || input[idx - 1] == b':') {
return None;
}
let start = idx;
let (_, mut end) = crate::ip::Ip::run_bounds(input, idx, memo);
while end > start && input[end - 1] == b':' && !(end >= 2 && input[end - 2] == b':') {
end -= 1;
}
if end - start > crate::ipv6::MAX_IPV6_LEN {
return None;
}
let candidate = &input[start..end];
if !candidate.contains(&b':') {
return None;
}
if !crate::ipv6::is_valid_ipv6(candidate) {
return None;
}
(start, end)
};
if ip_end >= input.len() || input[ip_end] != b'/' {
return None;
}
let mut pos = ip_end + 1;
let prefix_start = pos;
while pos < input.len() && input[pos].is_ascii_digit() {
pos += 1;
}
let prefix_len = pos - prefix_start;
if prefix_len == 0 || prefix_len > 3 {
return None;
}
if prefix_len > 1 && input[prefix_start] == b'0' {
return None;
}
let mut prefix: u16 = 0;
for &b in &input[prefix_start..pos] {
prefix = prefix * 10 + (b - b'0') as u16;
}
if prefix > 128 {
return None;
}
if pos < input.len() {
if input[pos].is_ascii_digit() {
return None;
}
if input[pos] == b'.' && pos + 1 < input.len() && input[pos + 1].is_ascii_digit() {
return None;
}
}
Some(ip_start..pos)
}
}
impl Finder for Cidr {
fn line_agnostic(&self) -> bool {
true
}
fn id(&self) -> &'static str {
"cidr"
}
fn dispatchable(&self) -> bool {
true
}
fn could_start_at(&self, byte: u8) -> bool {
byte.is_ascii_hexdigit() || byte == b':' || byte == b'['
}
fn could_start_after(&self, prev: u8, cur: u8) -> bool {
if cur == b'[' {
return true;
}
let v6 = (cur.is_ascii_hexdigit() || cur == b':')
&& !(prev.is_ascii_alphanumeric() || prev == b':');
let v4 = cur.is_ascii_digit() && !(prev.is_ascii_digit() || prev == b'.' || prev == b':');
v6 || v4
}
fn could_continue_with(&self, cur: u8, next: u8) -> bool {
cur == b'[' || next.is_ascii_hexdigit() || next == b':' || next == b'.'
}
fn anchor(&self) -> Option<Anchor> {
Some(Anchor::new(
ByteSet::from_bytes(b"/"),
ByteSet::from_fn(|b| b.is_ascii_hexdigit() || matches!(b, b'.' | b':' | b'[' | b']')),
))
}
fn run_rules(&self) -> Vec<RunRule> {
vec![
RunRule::new(RunClass::Digit, 1, 3)
.followed_by(b".")
.then(RunClass::Digit, 1, 3)
.followed_by(b"."),
RunRule::new(RunClass::Hex, 1, 4)
.followed_by(b":")
.then(RunClass::Hex, 1, 4)
.followed_by(b":")
.then(RunClass::Hex, 0, 4)
.followed_by(b":")
.or_end(),
RunRule::new(RunClass::Hex, 1, 4)
.followed_by(b":")
.then(RunClass::Hex, 0, 0)
.followed_by(b":"),
]
}
fn try_at(&self, input: &[u8], pos: usize) -> Option<Range<usize>> {
self.try_at_memo(input, pos, &mut Memo::default())
}
fn try_at_memo(&self, input: &[u8], pos: usize, memo: &mut Memo) -> Option<Range<usize>> {
if input[pos].is_ascii_digit()
&& let Some(range) = Self::try_ipv4_cidr(input, pos)
{
return Some(range);
}
if (input[pos] == b'[' || input[pos].is_ascii_hexdigit() || input[pos] == b':')
&& let Some(range) = Self::try_ipv6_cidr(input, pos, memo)
{
return Some(range);
}
None
}
fn find(&self, s: &str) -> Option<Range<usize>> {
let input = s.as_bytes();
let mut idx = 0;
while idx < input.len() {
if input[idx].is_ascii_digit()
&& let Some(range) = Self::try_ipv4_cidr(input, idx)
{
return Some(range);
}
if (input[idx] == b'[' || input[idx].is_ascii_hexdigit() || input[idx] == b':')
&& let Some(range) = Self::try_ipv6_cidr(input, idx, &mut Memo::default())
{
return Some(range);
}
idx += 1;
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn id_should_return_cidr() {
let finder = Cidr::default();
assert_eq!("cidr", finder.id());
}
#[test]
fn find_should_extract_ipv4_cidr() {
let finder = Cidr::default();
let input = "network 192.168.1.0/24 is local";
let range = finder.find(input).unwrap();
assert_eq!("192.168.1.0/24", &input[range]);
}
#[test]
fn find_should_extract_class_a() {
let finder = Cidr::default();
let input = "10.0.0.0/8";
let range = finder.find(input).unwrap();
assert_eq!("10.0.0.0/8", &input[range]);
}
#[test]
fn find_should_extract_host_route() {
let finder = Cidr::default();
let input = "host 10.0.0.1/32 route";
let range = finder.find(input).unwrap();
assert_eq!("10.0.0.1/32", &input[range]);
}
#[test]
fn find_should_extract_default_route() {
let finder = Cidr::default();
let input = "0.0.0.0/0";
let range = finder.find(input).unwrap();
assert_eq!("0.0.0.0/0", &input[range]);
}
#[test]
fn find_should_reject_prefix_over_32() {
let finder = Cidr::default();
assert!(finder.find("192.168.1.0/33").is_none());
}
#[test]
fn find_should_reject_no_prefix() {
let finder = Cidr::default();
assert!(finder.find("only 192.168.1.0 here").is_none());
}
#[test]
fn find_should_reject_invalid_octet() {
let finder = Cidr::default();
assert!(finder.find("256.168.1.0/24").is_none());
}
#[test]
fn find_should_reject_leading_zero_octet() {
let finder = Cidr::default();
assert!(finder.find("192.168.01.0/24").is_none());
}
#[test]
fn find_should_reject_leading_zero_prefix() {
let finder = Cidr::default();
assert!(finder.find("192.168.1.0/08").is_none());
}
#[test]
fn find_should_reject_preceded_by_digit() {
let finder = Cidr::default();
assert!(finder.find("x1192.168.1.0/24").is_none());
}
#[test]
fn find_should_reject_followed_by_digit() {
let finder = Cidr::default();
assert!(finder.find("192.168.1.0/245").is_none());
}
#[test]
fn find_should_extract_ipv6_cidr() {
let finder = Cidr::default();
let input = "network 2001:db8::/32 configured";
let range = finder.find(input).unwrap();
assert_eq!("2001:db8::/32", &input[range]);
}
#[test]
fn find_should_extract_ipv6_full_cidr() {
let finder = Cidr::default();
let input = "2001:0db8:85a3:0000:0000:8a2e:0370:7334/64";
let range = finder.find(input).unwrap();
assert_eq!("2001:0db8:85a3:0000:0000:8a2e:0370:7334/64", &input[range]);
}
#[test]
fn find_should_extract_ipv6_loopback_cidr() {
let finder = Cidr::default();
let input = "::1/128";
let range = finder.find(input).unwrap();
assert_eq!("::1/128", &input[range]);
}
#[test]
fn find_should_extract_ipv6_default_cidr() {
let finder = Cidr::default();
let input = "::/0";
let range = finder.find(input).unwrap();
assert_eq!("::/0", &input[range]);
}
#[test]
fn find_should_reject_ipv6_prefix_over_128() {
let finder = Cidr::default();
assert!(finder.find("2001:db8::/129").is_none());
}
#[test]
fn find_should_extract_multiple_cidrs_iteratively() {
let finder = Cidr::default();
let input = "10.0.0.0/8 and 192.168.1.0/24";
let mut results = Vec::new();
let mut idx = 0;
while idx < input.len() {
if let Some(range) = finder.find(&input[idx..]) {
results.push(&input[idx + range.start..idx + range.end]);
idx += range.end;
} else {
break;
}
}
assert_eq!(vec!["10.0.0.0/8", "192.168.1.0/24"], results);
}
#[test]
fn find_should_handle_empty_input() {
let finder = Cidr::default();
assert!(finder.find("").is_none());
}
#[test]
fn find_should_extract_cidr_at_start() {
let finder = Cidr::default();
let input = "10.0.0.0/8 is class A";
let range = finder.find(input).unwrap();
assert_eq!("10.0.0.0/8", &input[range]);
}
#[test]
fn find_should_extract_cidr_at_end() {
let finder = Cidr::default();
let input = "subnet is 172.16.0.0/12";
let range = finder.find(input).unwrap();
assert_eq!("172.16.0.0/12", &input[range]);
}
#[test]
fn try_at_ipv4_cidr() {
let finder = Cidr::default();
let input = b"192.168.1.0/24 rest";
assert_eq!(finder.try_at(input, 0), Some(0..14));
}
#[test]
fn try_at_ipv6_cidr() {
let finder = Cidr::default();
let input = b"2001:db8::/32 rest";
assert_eq!(finder.try_at(input, 0), Some(0..13));
}
#[test]
fn try_at_no_prefix() {
let finder = Cidr::default();
let input = b"192.168.1.0 no prefix";
assert!(finder.try_at(input, 0).is_none());
}
#[test]
fn try_at_non_digit() {
let finder = Cidr::default();
assert!(finder.try_at(b"abc", 0).is_none());
}
#[test]
fn try_at_single_digit() {
let finder = Cidr::default();
assert!(finder.try_at(b"1", 0).is_none());
}
#[test]
fn find_prefix_zero() {
let finder = Cidr::default();
let input = "0.0.0.0/0";
let range = finder.find(input).unwrap();
assert_eq!("0.0.0.0/0", &input[range]);
}
#[test]
fn find_prefix_32() {
let finder = Cidr::default();
let input = "10.0.0.1/32";
let range = finder.find(input).unwrap();
assert_eq!("10.0.0.1/32", &input[range]);
}
#[test]
fn find_rejects_prefix_with_leading_zero() {
let finder = Cidr::default();
assert!(finder.find("10.0.0.0/08").is_none());
}
#[test]
fn find_ipv6_prefix_128() {
let finder = Cidr::default();
let input = "::1/128";
let range = finder.find(input).unwrap();
assert_eq!("::1/128", &input[range]);
}
#[test]
fn find_ipv6_prefix_zero() {
let finder = Cidr::default();
let input = "::/0";
let range = finder.find(input).unwrap();
assert_eq!("::/0", &input[range]);
}
#[test]
fn could_start_at_digit() {
let finder = Cidr::default();
for b in b'0'..=b'9' {
assert!(
finder.could_start_at(b),
"should start at digit {}",
b as char
);
}
}
#[test]
fn could_start_at_hex_alpha() {
let finder = Cidr::default();
for b in b'a'..=b'f' {
assert!(finder.could_start_at(b), "should start at {}", b as char);
}
for b in b'A'..=b'F' {
assert!(finder.could_start_at(b), "should start at {}", b as char);
}
}
#[test]
fn could_start_at_colon_bracket() {
let finder = Cidr::default();
assert!(finder.could_start_at(b':'));
assert!(finder.could_start_at(b'['));
}
#[test]
fn could_not_start_at_non_hex() {
let finder = Cidr::default();
assert!(!finder.could_start_at(b'g'));
assert!(!finder.could_start_at(b' '));
assert!(!finder.could_start_at(b'/'));
}
}