Skip to main content

squeeze/
cidr.rs

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        // Boundary before: not preceded by digit or dot, nor by ':' — a quad
14        // after a colon is the leftover interior of a failed IPv6 candidate
15        // (`1::2::1.2.3.4/24`) and must not be mined.
16        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        // Parse 4 octets
26        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        // Must have /prefix
57        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        // No leading zeros in prefix
72        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        // Boundary after: a digit would extend the prefix; a '.' only vetoes
85        // when followed by a digit (`/24.5`). A bare '.' is sentence
86        // punctuation (`10.0.0.0/8. done`) — a valid CIDR cannot continue
87        // with '.' + non-digit.
88        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        // Look for IPv6 addresses followed by /prefix
102        // Handle both bare and bracketed forms
103        let (ip_start, ip_end) = if input[idx] == b'[' {
104            // Bounded like the ip finder: a farther bracket cannot close a
105            // valid address.
106            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            // Boundary before
119            if idx > 0 && (input[idx - 1].is_ascii_alphanumeric() || input[idx - 1] == b':') {
120                return None;
121            }
122
123            let start = idx;
124            // '.' is part of the charset so embedded IPv4 tails
125            // (`::ffff:0.0.0.0/96`, `64:ff9b::192.0.2.33/24`) are consumed
126            // whole instead of leaving a bogus v4 leftover. The run is
127            // shared through the memo with every candidate inside it.
128            let (_, mut end) = crate::ip::Ip::run_bounds(input, idx, memo);
129
130            // Strip trailing colons (except ::)
131            while end > start && input[end - 1] == b':' && !(end >= 2 && input[end - 2] == b':') {
132                end -= 1;
133            }
134
135            // Longer than any address: reject before scanning the candidate.
136            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        // Must have /prefix
150        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        // Boundary after: same rule as the IPv4 prefix — reject a digit or a
177        // '.' followed by a digit, allow a sentence dot (`2001:db8::/32.`).
178        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        // Matches never contain a line terminator and `\n`/`\r` end every
194        // walk exactly like the end of the input does.
195        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        // The whole address precedes the `/`, brackets included.
226        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    // IPv4 CIDR
305    #[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    // IPv6 CIDR
380    #[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    // Multiple
419    #[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    // --- Regression: could_start_at without redundant digit check ---
532
533    #[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}