1use super::{Anchor, ByteSet, Finder, Memo, RunClass, RunRule};
2use std::ops::Range;
3
4#[derive(Default)]
5pub struct Cidr {}
6
7impl Cidr {
8 fn try_ipv4_cidr(input: &[u8], idx: usize) -> Option<Range<usize>> {
9 if !input[idx].is_ascii_digit() {
10 return None;
11 }
12
13 if idx > 0
17 && (input[idx - 1].is_ascii_digit() || input[idx - 1] == b'.' || input[idx - 1] == b':')
18 {
19 return None;
20 }
21
22 let start = idx;
23 let mut pos = idx;
24
25 for octet_idx in 0..4 {
27 if octet_idx > 0 {
28 if pos >= input.len() || input[pos] != b'.' {
29 return None;
30 }
31 pos += 1;
32 }
33
34 let octet_start = pos;
35 while pos < input.len() && input[pos].is_ascii_digit() {
36 pos += 1;
37 }
38 let octet_len = pos - octet_start;
39 if octet_len == 0 || octet_len > 3 {
40 return None;
41 }
42
43 if octet_len > 1 && input[octet_start] == b'0' {
44 return None;
45 }
46
47 let mut value: u16 = 0;
48 for &b in &input[octet_start..pos] {
49 value = value * 10 + (b - b'0') as u16;
50 }
51 if value > 255 {
52 return None;
53 }
54 }
55
56 if pos >= input.len() || input[pos] != b'/' {
58 return None;
59 }
60 pos += 1;
61
62 let prefix_start = pos;
63 while pos < input.len() && input[pos].is_ascii_digit() {
64 pos += 1;
65 }
66 let prefix_len = pos - prefix_start;
67 if prefix_len == 0 || prefix_len > 2 {
68 return None;
69 }
70
71 if prefix_len > 1 && input[prefix_start] == b'0' {
73 return None;
74 }
75
76 let mut prefix: u8 = 0;
77 for &b in &input[prefix_start..pos] {
78 prefix = prefix * 10 + (b - b'0');
79 }
80 if prefix > 32 {
81 return None;
82 }
83
84 if pos < input.len() {
89 if input[pos].is_ascii_digit() {
90 return None;
91 }
92 if input[pos] == b'.' && pos + 1 < input.len() && input[pos + 1].is_ascii_digit() {
93 return None;
94 }
95 }
96
97 Some(start..pos)
98 }
99
100 fn try_ipv6_cidr(input: &[u8], idx: usize, memo: &mut Memo) -> Option<Range<usize>> {
101 let (ip_start, ip_end) = if input[idx] == b'[' {
104 let window = &input[idx..input.len().min(idx + 47)];
107 let close = window.iter().position(|&b| b == b']')?;
108 let close_pos = idx + close;
109 let inner = &input[idx + 1..close_pos];
110 if !crate::ipv6::is_valid_ipv6(inner) {
111 return None;
112 }
113 (idx, close_pos + 1)
114 } else {
115 if !input[idx].is_ascii_hexdigit() && input[idx] != b':' {
116 return None;
117 }
118 if idx > 0 && (input[idx - 1].is_ascii_alphanumeric() || input[idx - 1] == b':') {
120 return None;
121 }
122
123 let start = idx;
124 let (_, mut end) = crate::ip::Ip::run_bounds(input, idx, memo);
129
130 while end > start && input[end - 1] == b':' && !(end >= 2 && input[end - 2] == b':') {
132 end -= 1;
133 }
134
135 if end - start > crate::ipv6::MAX_IPV6_LEN {
137 return None;
138 }
139 let candidate = &input[start..end];
140 if !candidate.contains(&b':') {
141 return None;
142 }
143 if !crate::ipv6::is_valid_ipv6(candidate) {
144 return None;
145 }
146 (start, end)
147 };
148
149 if ip_end >= input.len() || input[ip_end] != b'/' {
151 return None;
152 }
153 let mut pos = ip_end + 1;
154
155 let prefix_start = pos;
156 while pos < input.len() && input[pos].is_ascii_digit() {
157 pos += 1;
158 }
159 let prefix_len = pos - prefix_start;
160 if prefix_len == 0 || prefix_len > 3 {
161 return None;
162 }
163
164 if prefix_len > 1 && input[prefix_start] == b'0' {
165 return None;
166 }
167
168 let mut prefix: u16 = 0;
169 for &b in &input[prefix_start..pos] {
170 prefix = prefix * 10 + (b - b'0') as u16;
171 }
172 if prefix > 128 {
173 return None;
174 }
175
176 if pos < input.len() {
179 if input[pos].is_ascii_digit() {
180 return None;
181 }
182 if input[pos] == b'.' && pos + 1 < input.len() && input[pos + 1].is_ascii_digit() {
183 return None;
184 }
185 }
186
187 Some(ip_start..pos)
188 }
189}
190
191impl Finder for Cidr {
192 fn line_agnostic(&self) -> bool {
193 true
196 }
197
198 fn id(&self) -> &'static str {
199 "cidr"
200 }
201
202 fn dispatchable(&self) -> bool {
203 true
204 }
205
206 fn could_start_at(&self, byte: u8) -> bool {
207 byte.is_ascii_hexdigit() || byte == b':' || byte == b'['
208 }
209
210 fn could_start_after(&self, prev: u8, cur: u8) -> bool {
211 if cur == b'[' {
212 return true;
213 }
214 let v6 = (cur.is_ascii_hexdigit() || cur == b':')
215 && !(prev.is_ascii_alphanumeric() || prev == b':');
216 let v4 = cur.is_ascii_digit() && !(prev.is_ascii_digit() || prev == b'.' || prev == b':');
217 v6 || v4
218 }
219
220 fn could_continue_with(&self, cur: u8, next: u8) -> bool {
221 cur == b'[' || next.is_ascii_hexdigit() || next == b':' || next == b'.'
222 }
223
224 fn anchor(&self) -> Option<Anchor> {
225 Some(Anchor::new(
227 ByteSet::from_bytes(b"/"),
228 ByteSet::from_fn(|b| b.is_ascii_hexdigit() || matches!(b, b'.' | b':' | b'[' | b']')),
229 ))
230 }
231
232 fn run_rules(&self) -> Vec<RunRule> {
233 vec![
234 RunRule::new(RunClass::Digit, 1, 3)
235 .followed_by(b".")
236 .then(RunClass::Digit, 1, 3)
237 .followed_by(b"."),
238 RunRule::new(RunClass::Hex, 1, 4)
239 .followed_by(b":")
240 .then(RunClass::Hex, 1, 4)
241 .followed_by(b":")
242 .then(RunClass::Hex, 0, 4)
243 .followed_by(b":")
244 .or_end(),
245 RunRule::new(RunClass::Hex, 1, 4)
246 .followed_by(b":")
247 .then(RunClass::Hex, 0, 0)
248 .followed_by(b":"),
249 ]
250 }
251
252 fn try_at(&self, input: &[u8], pos: usize) -> Option<Range<usize>> {
253 self.try_at_memo(input, pos, &mut Memo::default())
254 }
255
256 fn try_at_memo(&self, input: &[u8], pos: usize, memo: &mut Memo) -> Option<Range<usize>> {
257 if input[pos].is_ascii_digit()
258 && let Some(range) = Self::try_ipv4_cidr(input, pos)
259 {
260 return Some(range);
261 }
262 if (input[pos] == b'[' || input[pos].is_ascii_hexdigit() || input[pos] == b':')
263 && let Some(range) = Self::try_ipv6_cidr(input, pos, memo)
264 {
265 return Some(range);
266 }
267 None
268 }
269
270 fn find(&self, s: &str) -> Option<Range<usize>> {
271 let input = s.as_bytes();
272 let mut idx = 0;
273
274 while idx < input.len() {
275 if input[idx].is_ascii_digit()
276 && let Some(range) = Self::try_ipv4_cidr(input, idx)
277 {
278 return Some(range);
279 }
280
281 if (input[idx] == b'[' || input[idx].is_ascii_hexdigit() || input[idx] == b':')
282 && let Some(range) = Self::try_ipv6_cidr(input, idx, &mut Memo::default())
283 {
284 return Some(range);
285 }
286
287 idx += 1;
288 }
289
290 None
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[test]
299 fn id_should_return_cidr() {
300 let finder = Cidr::default();
301 assert_eq!("cidr", finder.id());
302 }
303
304 #[test]
306 fn find_should_extract_ipv4_cidr() {
307 let finder = Cidr::default();
308 let input = "network 192.168.1.0/24 is local";
309 let range = finder.find(input).unwrap();
310 assert_eq!("192.168.1.0/24", &input[range]);
311 }
312
313 #[test]
314 fn find_should_extract_class_a() {
315 let finder = Cidr::default();
316 let input = "10.0.0.0/8";
317 let range = finder.find(input).unwrap();
318 assert_eq!("10.0.0.0/8", &input[range]);
319 }
320
321 #[test]
322 fn find_should_extract_host_route() {
323 let finder = Cidr::default();
324 let input = "host 10.0.0.1/32 route";
325 let range = finder.find(input).unwrap();
326 assert_eq!("10.0.0.1/32", &input[range]);
327 }
328
329 #[test]
330 fn find_should_extract_default_route() {
331 let finder = Cidr::default();
332 let input = "0.0.0.0/0";
333 let range = finder.find(input).unwrap();
334 assert_eq!("0.0.0.0/0", &input[range]);
335 }
336
337 #[test]
338 fn find_should_reject_prefix_over_32() {
339 let finder = Cidr::default();
340 assert!(finder.find("192.168.1.0/33").is_none());
341 }
342
343 #[test]
344 fn find_should_reject_no_prefix() {
345 let finder = Cidr::default();
346 assert!(finder.find("only 192.168.1.0 here").is_none());
347 }
348
349 #[test]
350 fn find_should_reject_invalid_octet() {
351 let finder = Cidr::default();
352 assert!(finder.find("256.168.1.0/24").is_none());
353 }
354
355 #[test]
356 fn find_should_reject_leading_zero_octet() {
357 let finder = Cidr::default();
358 assert!(finder.find("192.168.01.0/24").is_none());
359 }
360
361 #[test]
362 fn find_should_reject_leading_zero_prefix() {
363 let finder = Cidr::default();
364 assert!(finder.find("192.168.1.0/08").is_none());
365 }
366
367 #[test]
368 fn find_should_reject_preceded_by_digit() {
369 let finder = Cidr::default();
370 assert!(finder.find("x1192.168.1.0/24").is_none());
371 }
372
373 #[test]
374 fn find_should_reject_followed_by_digit() {
375 let finder = Cidr::default();
376 assert!(finder.find("192.168.1.0/245").is_none());
377 }
378
379 #[test]
381 fn find_should_extract_ipv6_cidr() {
382 let finder = Cidr::default();
383 let input = "network 2001:db8::/32 configured";
384 let range = finder.find(input).unwrap();
385 assert_eq!("2001:db8::/32", &input[range]);
386 }
387
388 #[test]
389 fn find_should_extract_ipv6_full_cidr() {
390 let finder = Cidr::default();
391 let input = "2001:0db8:85a3:0000:0000:8a2e:0370:7334/64";
392 let range = finder.find(input).unwrap();
393 assert_eq!("2001:0db8:85a3:0000:0000:8a2e:0370:7334/64", &input[range]);
394 }
395
396 #[test]
397 fn find_should_extract_ipv6_loopback_cidr() {
398 let finder = Cidr::default();
399 let input = "::1/128";
400 let range = finder.find(input).unwrap();
401 assert_eq!("::1/128", &input[range]);
402 }
403
404 #[test]
405 fn find_should_extract_ipv6_default_cidr() {
406 let finder = Cidr::default();
407 let input = "::/0";
408 let range = finder.find(input).unwrap();
409 assert_eq!("::/0", &input[range]);
410 }
411
412 #[test]
413 fn find_should_reject_ipv6_prefix_over_128() {
414 let finder = Cidr::default();
415 assert!(finder.find("2001:db8::/129").is_none());
416 }
417
418 #[test]
420 fn find_should_extract_multiple_cidrs_iteratively() {
421 let finder = Cidr::default();
422 let input = "10.0.0.0/8 and 192.168.1.0/24";
423
424 let mut results = Vec::new();
425 let mut idx = 0;
426 while idx < input.len() {
427 if let Some(range) = finder.find(&input[idx..]) {
428 results.push(&input[idx + range.start..idx + range.end]);
429 idx += range.end;
430 } else {
431 break;
432 }
433 }
434
435 assert_eq!(vec!["10.0.0.0/8", "192.168.1.0/24"], results);
436 }
437
438 #[test]
439 fn find_should_handle_empty_input() {
440 let finder = Cidr::default();
441 assert!(finder.find("").is_none());
442 }
443
444 #[test]
445 fn find_should_extract_cidr_at_start() {
446 let finder = Cidr::default();
447 let input = "10.0.0.0/8 is class A";
448 let range = finder.find(input).unwrap();
449 assert_eq!("10.0.0.0/8", &input[range]);
450 }
451
452 #[test]
453 fn find_should_extract_cidr_at_end() {
454 let finder = Cidr::default();
455 let input = "subnet is 172.16.0.0/12";
456 let range = finder.find(input).unwrap();
457 assert_eq!("172.16.0.0/12", &input[range]);
458 }
459
460 #[test]
461 fn try_at_ipv4_cidr() {
462 let finder = Cidr::default();
463 let input = b"192.168.1.0/24 rest";
464 assert_eq!(finder.try_at(input, 0), Some(0..14));
465 }
466
467 #[test]
468 fn try_at_ipv6_cidr() {
469 let finder = Cidr::default();
470 let input = b"2001:db8::/32 rest";
471 assert_eq!(finder.try_at(input, 0), Some(0..13));
472 }
473
474 #[test]
475 fn try_at_no_prefix() {
476 let finder = Cidr::default();
477 let input = b"192.168.1.0 no prefix";
478 assert!(finder.try_at(input, 0).is_none());
479 }
480
481 #[test]
482 fn try_at_non_digit() {
483 let finder = Cidr::default();
484 assert!(finder.try_at(b"abc", 0).is_none());
485 }
486
487 #[test]
488 fn try_at_single_digit() {
489 let finder = Cidr::default();
490 assert!(finder.try_at(b"1", 0).is_none());
491 }
492
493 #[test]
494 fn find_prefix_zero() {
495 let finder = Cidr::default();
496 let input = "0.0.0.0/0";
497 let range = finder.find(input).unwrap();
498 assert_eq!("0.0.0.0/0", &input[range]);
499 }
500
501 #[test]
502 fn find_prefix_32() {
503 let finder = Cidr::default();
504 let input = "10.0.0.1/32";
505 let range = finder.find(input).unwrap();
506 assert_eq!("10.0.0.1/32", &input[range]);
507 }
508
509 #[test]
510 fn find_rejects_prefix_with_leading_zero() {
511 let finder = Cidr::default();
512 assert!(finder.find("10.0.0.0/08").is_none());
513 }
514
515 #[test]
516 fn find_ipv6_prefix_128() {
517 let finder = Cidr::default();
518 let input = "::1/128";
519 let range = finder.find(input).unwrap();
520 assert_eq!("::1/128", &input[range]);
521 }
522
523 #[test]
524 fn find_ipv6_prefix_zero() {
525 let finder = Cidr::default();
526 let input = "::/0";
527 let range = finder.find(input).unwrap();
528 assert_eq!("::/0", &input[range]);
529 }
530
531 #[test]
534 fn could_start_at_digit() {
535 let finder = Cidr::default();
536 for b in b'0'..=b'9' {
537 assert!(
538 finder.could_start_at(b),
539 "should start at digit {}",
540 b as char
541 );
542 }
543 }
544
545 #[test]
546 fn could_start_at_hex_alpha() {
547 let finder = Cidr::default();
548 for b in b'a'..=b'f' {
549 assert!(finder.could_start_at(b), "should start at {}", b as char);
550 }
551 for b in b'A'..=b'F' {
552 assert!(finder.could_start_at(b), "should start at {}", b as char);
553 }
554 }
555
556 #[test]
557 fn could_start_at_colon_bracket() {
558 let finder = Cidr::default();
559 assert!(finder.could_start_at(b':'));
560 assert!(finder.could_start_at(b'['));
561 }
562
563 #[test]
564 fn could_not_start_at_non_hex() {
565 let finder = Cidr::default();
566 assert!(!finder.could_start_at(b'g'));
567 assert!(!finder.could_start_at(b' '));
568 assert!(!finder.could_start_at(b'/'));
569 }
570}