1use super::{Anchor, ByteSet, Finder, Memo, RunClass, RunRule};
2use std::ops::Range;
3
4pub struct Ip {
5 pub ipv4: bool,
6 pub ipv6: bool,
7}
8
9impl Default for Ip {
10 fn default() -> Self {
11 Ip {
12 ipv4: true,
13 ipv6: true,
14 }
15 }
16}
17
18const MAX_BRACKETED_IPV6: usize = 47;
20
21impl Ip {
22 pub(crate) fn run_bounds(input: &[u8], idx: usize, memo: &mut Memo) -> (usize, usize) {
26 if memo.covers(idx) {
27 return (memo.start, memo.end);
28 }
29 let is_run = |b: u8| b.is_ascii_hexdigit() || b == b':' || b == b'.';
30 let mut start = idx;
31 while start > 0 && is_run(input[start - 1]) {
32 start -= 1;
33 }
34 let mut end = idx;
35 while end < input.len() && is_run(input[end]) {
36 end += 1;
37 }
38 let mut stripped = end;
42 if stripped > start
43 && input[stripped - 1] == b':'
44 && !(stripped - start >= 2 && input[stripped - 2] == b':')
45 {
46 stripped -= 1;
47 }
48 while stripped > start && input[stripped - 1] == b'.' {
49 stripped -= 1;
50 }
51 *memo = Memo {
52 start,
53 end,
54 aux: stripped,
55 };
56 (start, end)
57 }
58
59 fn try_ipv4(input: &[u8], idx: usize, memo: &mut Memo) -> Option<Range<usize>> {
60 if !input[idx].is_ascii_digit() {
61 return None;
62 }
63 if idx > 0 && (input[idx - 1].is_ascii_digit() || input[idx - 1] == b'.') {
65 return None;
66 }
67 if idx > 0 && input[idx - 1] == b':' {
74 let (run_start, _) = Self::run_bounds(input, idx, memo);
75 if run_start == 0 || !input[run_start - 1].is_ascii_alphanumeric() {
76 return None;
77 }
78 }
79
80 let start = idx;
81 let mut pos = idx;
82
83 for octet_idx in 0..4 {
84 if octet_idx > 0 {
85 if pos >= input.len() || input[pos] != b'.' {
86 return None;
87 }
88 pos += 1;
89 }
90
91 let octet_start = pos;
92 while pos < input.len() && input[pos].is_ascii_digit() {
93 pos += 1;
94 }
95 let octet_len = pos - octet_start;
96 if octet_len == 0 || octet_len > 3 {
97 return None;
98 }
99
100 if octet_len > 1 && input[octet_start] == b'0' {
102 return None;
103 }
104
105 let mut value: u16 = 0;
106 for &b in &input[octet_start..pos] {
107 value = value * 10 + (b - b'0') as u16;
108 }
109 if value > 255 {
110 return None;
111 }
112 }
113
114 if pos < input.len() && (input[pos].is_ascii_digit() || input[pos] == b'.') {
116 return None;
117 }
118
119 Some(start..pos)
120 }
121
122 fn try_bracketed_ipv6(input: &[u8], idx: usize) -> Option<Range<usize>> {
123 if input[idx] != b'[' {
124 return None;
125 }
126
127 let window = &input[idx..input.len().min(idx + MAX_BRACKETED_IPV6)];
130 let close = window.iter().position(|&b| b == b']')?;
131 let close_pos = idx + close;
132 let inner = &input[idx + 1..close_pos];
133
134 if crate::ipv6::is_valid_ipv6(inner) {
135 Some(idx..close_pos + 1)
136 } else {
137 None
138 }
139 }
140
141 fn try_bare_ipv6(input: &[u8], idx: usize, memo: &mut Memo) -> Option<Range<usize>> {
142 if !input[idx].is_ascii_hexdigit() && input[idx] != b':' {
144 return None;
145 }
146
147 if idx > 0 && (input[idx - 1].is_ascii_alphanumeric() || input[idx - 1] == b':') {
149 return None;
150 }
151
152 let start = idx;
153 let (_, mut end) = Self::run_bounds(input, idx, memo);
154
155 if end < input.len() && input[end].is_ascii_alphanumeric() {
160 return None;
161 }
162
163 while end > start && input[end - 1] == b':' && !(end - start >= 2 && input[end - 2] == b':')
166 {
167 end -= 1;
168 }
169
170 let dot_end = memo.aux.clamp(start, end);
175
176 if dot_end - start > crate::ipv6::MAX_IPV6_LEN {
180 return None;
181 }
182
183 if !input[start..dot_end].contains(&b':') {
186 return None;
187 }
188
189 if crate::ipv6::is_valid_ipv6(&input[start..end]) {
190 return Some(start..end);
191 }
192
193 if dot_end < end && crate::ipv6::is_valid_ipv6(&input[start..dot_end]) {
194 return Some(start..dot_end);
195 }
196
197 None
198 }
199}
200
201impl Finder for Ip {
202 fn line_agnostic(&self) -> bool {
203 true
206 }
207
208 fn id(&self) -> &'static str {
209 "ip"
210 }
211
212 fn dispatchable(&self) -> bool {
213 true
214 }
215
216 fn could_start_at(&self, byte: u8) -> bool {
217 (self.ipv4 && byte.is_ascii_digit())
218 || (self.ipv6 && (byte.is_ascii_hexdigit() || byte == b':' || byte == b'['))
219 }
220
221 fn could_start_after(&self, prev: u8, cur: u8) -> bool {
222 if cur == b'[' {
223 return true;
224 }
225 let v6 = self.ipv6
226 && (cur.is_ascii_hexdigit() || cur == b':')
227 && !(prev.is_ascii_alphanumeric() || prev == b':');
228 let v4 = self.ipv4 && cur.is_ascii_digit() && !(prev.is_ascii_digit() || prev == b'.');
229 v6 || v4
230 }
231
232 fn could_continue_with(&self, cur: u8, next: u8) -> bool {
233 cur == b'[' || next.is_ascii_hexdigit() || next == b':' || next == b'.'
234 }
235
236 fn anchor(&self) -> Option<Anchor> {
237 let mut bytes = ByteSet::EMPTY;
238 if self.ipv4 {
239 bytes = bytes.with(b'.');
240 }
241 if self.ipv6 {
242 bytes = bytes.with(b':');
243 }
244 let mut anchor = Anchor::new(
249 bytes,
250 ByteSet::from_fn(|b| b.is_ascii_hexdigit() || matches!(b, b'.' | b':' | b'[')),
251 );
252 if self.ipv4 {
253 anchor = anchor.confirm(b".", &[2, 3, 4], b".");
254 }
255 if self.ipv6 {
256 anchor = anchor.confirm(b":", &[1, 2, 3, 4, 5], b":");
257 }
258 Some(anchor)
259 }
260
261 fn run_rules(&self) -> Vec<RunRule> {
262 let mut rules = Vec::new();
263 if self.ipv4 {
264 rules.push(
266 RunRule::new(RunClass::Digit, 1, 3)
267 .followed_by(b".")
268 .then(RunClass::Digit, 1, 3)
269 .followed_by(b"."),
270 );
271 }
272 if self.ipv6 {
273 rules.push(
276 RunRule::new(RunClass::Hex, 1, 4)
277 .followed_by(b":")
278 .then(RunClass::Hex, 1, 4)
279 .followed_by(b":")
280 .then(RunClass::Hex, 0, 4)
281 .followed_by(b":")
282 .or_end(),
283 );
284 rules.push(
285 RunRule::new(RunClass::Hex, 1, 4)
286 .followed_by(b":")
287 .then(RunClass::Hex, 0, 0)
288 .followed_by(b":"),
289 );
290 }
291 rules
292 }
293
294 fn try_at(&self, input: &[u8], pos: usize) -> Option<Range<usize>> {
295 self.try_at_memo(input, pos, &mut Memo::default())
296 }
297
298 fn try_at_memo(&self, input: &[u8], pos: usize, memo: &mut Memo) -> Option<Range<usize>> {
299 if self.ipv6 {
300 if input[pos] == b'['
301 && let Some(range) = Self::try_bracketed_ipv6(input, pos)
302 {
303 return Some(range);
304 }
305 if (input[pos].is_ascii_hexdigit() || input[pos] == b':')
306 && let Some(range) = Self::try_bare_ipv6(input, pos, memo)
307 {
308 return Some(range);
309 }
310 }
311 if self.ipv4
312 && input[pos].is_ascii_digit()
313 && let Some(range) = Self::try_ipv4(input, pos, memo)
314 {
315 return Some(range);
316 }
317 None
318 }
319
320 fn find(&self, s: &str) -> Option<Range<usize>> {
321 let input = s.as_bytes();
322 let mut idx = 0;
323
324 while idx < input.len() {
325 if self.ipv6 {
326 if input[idx] == b'['
327 && let Some(range) = Self::try_bracketed_ipv6(input, idx)
328 {
329 return Some(range);
330 }
331
332 if (input[idx].is_ascii_hexdigit() || input[idx] == b':')
333 && let Some(range) = Self::try_bare_ipv6(input, idx, &mut Memo::default())
334 {
335 return Some(range);
336 }
337 }
338
339 if self.ipv4
340 && input[idx].is_ascii_digit()
341 && let Some(range) = Self::try_ipv4(input, idx, &mut Memo::default())
342 {
343 return Some(range);
344 }
345
346 idx += 1;
347 }
348
349 None
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356
357 #[test]
358 fn id_should_return_ip() {
359 let finder = Ip::default();
360 assert_eq!("ip", finder.id());
361 }
362
363 #[test]
365 fn find_should_extract_ipv4() {
366 let finder = Ip::default();
367 let input = "server at 192.168.1.1 running";
368 let range = finder.find(input).unwrap();
369 assert_eq!("192.168.1.1", &input[range]);
370 }
371
372 #[test]
373 fn find_should_extract_ipv4_at_start() {
374 let finder = Ip::default();
375 let input = "10.0.0.1 is gateway";
376 let range = finder.find(input).unwrap();
377 assert_eq!("10.0.0.1", &input[range]);
378 }
379
380 #[test]
381 fn find_should_extract_ipv4_at_end() {
382 let finder = Ip::default();
383 let input = "connect to 172.16.0.1";
384 let range = finder.find(input).unwrap();
385 assert_eq!("172.16.0.1", &input[range]);
386 }
387
388 #[test]
389 fn find_should_extract_localhost() {
390 let finder = Ip::default();
391 let input = "127.0.0.1";
392 let range = finder.find(input).unwrap();
393 assert_eq!("127.0.0.1", &input[range]);
394 }
395
396 #[test]
397 fn find_should_extract_all_zeros() {
398 let finder = Ip::default();
399 let input = "0.0.0.0";
400 let range = finder.find(input).unwrap();
401 assert_eq!("0.0.0.0", &input[range]);
402 }
403
404 #[test]
405 fn find_should_extract_max_values() {
406 let finder = Ip::default();
407 let input = "255.255.255.255";
408 let range = finder.find(input).unwrap();
409 assert_eq!("255.255.255.255", &input[range]);
410 }
411
412 #[test]
413 fn find_should_reject_octet_over_255() {
414 let finder = Ip::default();
415 assert!(finder.find("256.1.1.1").is_none());
416 }
417
418 #[test]
419 fn find_should_reject_three_octets() {
420 let finder = Ip::default();
421 assert!(finder.find("192.168.1 only").is_none());
422 }
423
424 #[test]
425 fn find_should_reject_five_octets() {
426 let finder = Ip::default();
427 assert!(finder.find("192.168.1.1.1").is_none());
428 }
429
430 #[test]
431 fn find_should_reject_leading_zeros() {
432 let finder = Ip::default();
433 assert!(finder.find("192.168.01.1").is_none());
434 }
435
436 #[test]
437 fn find_should_reject_preceded_by_digit() {
438 let finder = Ip::default();
439 assert!(finder.find("9192.168.1.1").is_none());
440 }
441
442 #[test]
444 fn find_should_extract_bracketed_ipv6() {
445 let finder = Ip::default();
446 let input = "connect to [::1] now";
447 let range = finder.find(input).unwrap();
448 assert_eq!("[::1]", &input[range]);
449 }
450
451 #[test]
452 fn find_should_extract_full_ipv6() {
453 let finder = Ip::default();
454 let input = "addr 2001:0db8:85a3:0000:0000:8a2e:0370:7334 here";
455 let range = finder.find(input).unwrap();
456 assert_eq!("2001:0db8:85a3:0000:0000:8a2e:0370:7334", &input[range]);
457 }
458
459 #[test]
460 fn find_should_extract_compressed_ipv6() {
461 let finder = Ip::default();
462 let input = "addr 2001:db8::1 here";
463 let range = finder.find(input).unwrap();
464 assert_eq!("2001:db8::1", &input[range]);
465 }
466
467 #[test]
468 fn find_should_extract_loopback_ipv6() {
469 let finder = Ip::default();
470 let input = "[::1]";
471 let range = finder.find(input).unwrap();
472 assert_eq!("[::1]", &input[range]);
473 }
474
475 #[test]
476 fn find_should_extract_bare_double_colon() {
477 let finder = Ip::default();
478 let input = "addr :: here";
479 let range = finder.find(input).unwrap();
480 assert_eq!("::", &input[range]);
481 }
482
483 #[test]
484 fn find_should_extract_ipv6_with_prefix_groups() {
485 let finder = Ip::default();
486 let input = "fe80::1";
487 let range = finder.find(input).unwrap();
488 assert_eq!("fe80::1", &input[range]);
489 }
490
491 #[test]
493 fn find_should_only_match_ipv4_when_configured() {
494 let finder = Ip {
495 ipv4: true,
496 ipv6: false,
497 };
498 let input = "192.168.1.1 and [::1]";
499 let range = finder.find(input).unwrap();
500 assert_eq!("192.168.1.1", &input[range]);
501 }
502
503 #[test]
504 fn find_should_only_match_ipv6_when_configured() {
505 let finder = Ip {
506 ipv4: false,
507 ipv6: true,
508 };
509 let input = "192.168.1.1 and [::1]";
510 let range = finder.find(input).unwrap();
511 assert_eq!("[::1]", &input[range]);
512 }
513
514 #[test]
516 fn find_should_extract_multiple_ips_iteratively() {
517 let finder = Ip::default();
518 let input = "10.0.0.1 and 10.0.0.2";
519
520 let mut results = Vec::new();
521 let mut idx = 0;
522 while idx < input.len() {
523 if let Some(range) = finder.find(&input[idx..]) {
524 results.push(&input[idx + range.start..idx + range.end]);
525 idx += range.end;
526 } else {
527 break;
528 }
529 }
530
531 assert_eq!(vec!["10.0.0.1", "10.0.0.2"], results);
532 }
533
534 #[test]
535 fn find_should_handle_empty_input() {
536 let finder = Ip::default();
537 assert!(finder.find("").is_none());
538 }
539
540 #[test]
541 fn find_should_handle_no_ip() {
542 let finder = Ip::default();
543 assert!(finder.find("just some text").is_none());
544 }
545
546 #[test]
547 fn try_at_ipv4_at_start() {
548 let finder = Ip::default();
549 let input = b"192.168.1.1 rest";
550 assert_eq!(finder.try_at(input, 0), Some(0..11));
551 }
552
553 #[test]
554 fn try_at_ipv4_mid_string() {
555 let finder = Ip::default();
556 let input = b"host 10.0.0.1 port";
557 assert_eq!(finder.try_at(input, 5), Some(5..13));
558 }
559
560 #[test]
561 fn try_at_rejects_non_digit() {
562 let finder = Ip::default();
563 let input = b"abc";
564 assert!(finder.try_at(input, 0).is_none());
565 }
566
567 #[test]
568 fn try_at_single_digit() {
569 let finder = Ip::default();
570 let input = b"1";
571 assert!(finder.try_at(input, 0).is_none());
572 }
573
574 #[test]
575 fn find_should_reject_empty_octets() {
576 let finder = Ip::default();
577 assert!(finder.find("192..1.1").is_none());
578 }
579
580 #[test]
581 fn find_should_reject_all_dots() {
582 let finder = Ip::default();
583 assert!(finder.find("...").is_none());
584 }
585
586 #[test]
587 fn try_at_bracketed_ipv6() {
588 let finder = Ip::default();
589 let input = b"[::1]";
590 assert_eq!(finder.try_at(input, 0), Some(0..5));
591 }
592
593 #[test]
594 fn find_ipv6_link_local() {
595 let finder = Ip::default();
596 let input = "addr fe80::1 here";
597 let range = finder.find(input).unwrap();
598 assert_eq!("fe80::1", &input[range]);
599 }
600}