ctoolbox/formats/
ip.rs

1/*
2Based on https://github.com/sindresorhus/ip-regex
3MIT License
4
5Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
6
7Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
8
9The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
10
11THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
12*/
13use fancy_regex::Regex;
14
15#[derive(Default, Clone)]
16struct Options {
17    /// Only match an exact string. Useful with `RegExp#test()` to check if a string is an IP address.
18    exact: bool,
19    /// Include boundaries in the regex. When true, 192.168.0.2000000000 will report as an invalid IPv4 address. If this option is not set, the mentioned IPv4 address would report as valid (ignoring the trailing zeros).
20    include_boundaries: bool,
21}
22
23enum RegexType {
24    V4,
25    V6,
26    V4V6,
27}
28
29fn get_regex(regex_type: &RegexType, options: Option<&Options>) -> String {
30    let options = options.cloned().unwrap_or_default();
31
32    // In the JavaScript version, include_boundaries has no effect when exact is true
33    assert!(
34        !(options.exact && options.include_boundaries),
35        "Cannot set both exact and include_boundaries options to true"
36    );
37
38    let word = "[a-fA-F\\d:]";
39
40    let boundary = if options.include_boundaries {
41        format!("(?:(?<=\\s|^)(?={word})|(?<={word})(?=\\s|$))")
42    } else {
43        String::new()
44    };
45
46    let v4 = "(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}";
47
48    let v6segment = "[a-fA-F\\d]{1,4}";
49
50    let v6 = format!(
51        r"
52(?:
53(?:{v6segment}:){{7}}(?:{v6segment}|:)|                                 
54    // 1:2:3:4:5:6:7::  1:2:3:4:5:6:7:8
55(?:{v6segment}:){{6}}(?:{v4}|:{v6segment}|:)|                           
56    // 1:2:3:4:5:6::    1:2:3:4:5:6::8   1:2:3:4:5:6::8  1:2:3:4:5:6::1.2.3.4
57(?:{v6segment}:){{5}}(?::{v4}|(?::{v6segment}){{1,2}}|:)|                   
58    // 1:2:3:4:5::      1:2:3:4:5::7:8   1:2:3:4:5::8    1:2:3:4:5::7:1.2.3.4
59(?:{v6segment}:){{4}}(?:(?::{v6segment}){{0,1}}:{v4}|(?::{v6segment}){{1,3}}|:)| 
60    // 1:2:3:4::        1:2:3:4::6:7:8   1:2:3:4::8      1:2:3:4::6:7:1.2.3.4
61(?:{v6segment}:){{3}}(?:(?::{v6segment}){{0,2}}:{v4}|(?::{v6segment}){{1,4}}|:)| 
62    // 1:2:3::          1:2:3::5:6:7:8   1:2:3::8        1:2:3::5:6:7:1.2.3.4
63(?:{v6segment}:){{2}}(?:(?::{v6segment}){{0,3}}:{v4}|(?::{v6segment}){{1,5}}|:)| 
64    // 1:2::            1:2::4:5:6:7:8   1:2::8          1:2::4:5:6:7:1.2.3.4
65(?:{v6segment}:){{1}}(?:(?::{v6segment}){{0,4}}:{v4}|(?::{v6segment}){{1,6}}|:)| 
66    // 1::              1::3:4:5:6:7:8   1::8            1::3:4:5:6:7:1.2.3.4
67(?::(?:(?::{v6segment}){{0,5}}:{v4}|(?::{v6segment}){{1,7}}|:))             
68    // ::2:3:4:5:6:7:8  ::2:3:4:5:6:7:8  ::8             ::1.2.3.4
69)(?:%[0-9a-zA-Z]{{1,}})?                                             
70    // %eth0            %1
71"
72    );
73    let v6 = normalize_v6(&v6);
74
75    // Pre-compile only the exact regexes because adding a global flag make regexes stateful
76    let v46_exact = format!(r"(?:^{v4}$)|(?:^{v6}$)");
77    let v4_exact = format!(r"^{v4}$");
78    let v6_exact = format!(r"^{v6}$");
79
80    let is_exact = options.exact;
81
82    let ip_regex = if is_exact {
83        v46_exact
84    } else {
85        format!(r"(?:{boundary}{v4}{boundary})|(?:{boundary}{v6}{boundary})")
86    };
87
88    let ip_regex_v4 = if is_exact {
89        v4_exact
90    } else {
91        format!(r"{boundary}{v4}{boundary}")
92    };
93    let ip_regex_v6 = if is_exact {
94        v6_exact
95    } else {
96        format!(r"{boundary}{v6}{boundary}")
97    };
98
99    match regex_type {
100        RegexType::V4 => ip_regex_v4,
101        RegexType::V6 => ip_regex_v6,
102        RegexType::V4V6 => ip_regex,
103    }
104}
105
106fn normalize_v6(v6: &str) -> String {
107    // remove //... to end-of-line (enable multiline mode so $ matches end of line)
108    let re_comments = Regex::new(r"(?m)\s*//.*$").expect("invalid regex");
109    let v6 = re_comments.replace_all(v6, "").to_string();
110    // remove newlines
111    let re_newline = Regex::new(r"\n").expect("invalid regex");
112    let v6 = re_newline.replace_all(&v6, "").to_string();
113    // trim
114    v6.trim().to_string()
115}
116
117/// Get a regex string that matches both IPv4 and IPv6 addresses exactly.
118/// If you run the regex against untrusted user input in a server context, you
119/// should give it a timeout.
120pub fn get_regex_ipv4_ipv6_exact() -> String {
121    get_regex(
122        &RegexType::V4V6,
123        Some(&Options {
124            exact: true,
125            include_boundaries: false,
126        }),
127    )
128}
129
130// NOTE: I haven't checked over these tests to make sure they match the original at https://raw.githubusercontent.com/sindresorhus/ip-regex/refs/heads/main/test.js
131
132#[cfg(test)]
133#[allow(clippy::unwrap_in_result, clippy::panic_in_result_fn)]
134mod tests {
135    use once_cell::sync::Lazy;
136
137    use super::*;
138    use std::collections::HashMap;
139    use std::sync::RwLock;
140
141    fn build_regex_rt(rt: &RegexType, options: Option<&Options>) -> Regex {
142        let pat = get_regex(rt, options);
143
144        // Fast path: try read lock and return cloned Regex if present
145        {
146            let cache = REGEX_CACHE.read().unwrap();
147            if let Some(re) = cache.get(&pat) {
148                return re.clone();
149            }
150        }
151
152        // Compile outside of write lock (may be done redundantly by multiple threads,
153        // but avoids blocking readers during compilation)
154        let invalid_msg = format!("invalid regex {pat}");
155        let compiled = Regex::new(&pat).expect(&invalid_msg);
156
157        // Insert into cache if absent (double-check to avoid overwriting another thread's insert)
158        let mut cache = REGEX_CACHE.write().unwrap();
159        let entry = cache.entry(pat).or_insert_with(|| compiled.clone());
160        entry.clone()
161    }
162
163    #[crate::ctb_test]
164    fn test_get_regex_ipv4_ipv6() {
165        let regex_str = get_regex_ipv4_ipv6_exact();
166        let regex = fancy_regex::Regex::new(&regex_str).unwrap();
167
168        // Valid IPv4 addresses
169        let valid_ipv4 = vec![
170            "127.0.0.1",
171            "0.0.0.0",
172            "255.255.255.255",
173            "192.168.1.1",
174            "1.2.3.4",
175            "10.0.0.1",
176        ];
177        for ip in valid_ipv4 {
178            assert!(
179                regex.is_match(ip).expect("Error"),
180                "expected '{}' to match IPv4 with regex '{}'",
181                ip,
182                regex.as_str()
183            );
184        }
185
186        // Valid IPv6 addresses (including IPv4-mapped)
187        let valid_ipv6 = vec![
188            "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
189            "2001:db8::1",
190            "::1",
191            "fe80::1ff:fe23:4567:890a",
192            "2001:db8:85a3::8a2e:370:7334",
193            "2001:0db8:0000:0000:0000:0000:1428:57ab",
194            "::ffff:192.0.2.128",
195        ];
196        for ip in valid_ipv6 {
197            assert!(
198                regex.is_match(ip).expect("Error"),
199                "expected '{}' to match IPv6 with regex '{}'",
200                ip,
201                regex.as_str()
202            );
203        }
204
205        // Invalid addresses
206        let invalid = vec![
207            "256.256.256.256",
208            "256.1.1.1",
209            "123.456.78.90",
210            "1.2.3",
211            "1.2.3.4.5",
212            "gibberish",
213            "2001::85a3::7334",
214            "2001:db8:85a3:8a2e:370:7334:1",
215            "",
216            ":",
217            ":::",
218        ];
219        for ip in invalid {
220            assert!(
221                !regex.is_match(ip).expect("Error"),
222                "expected '{}' to NOT match with regex '{}'",
223                ip,
224                regex.as_str()
225            );
226        }
227    }
228
229    // Global cache mapping pattern -> compiled Regex
230    static REGEX_CACHE: Lazy<RwLock<HashMap<String, Regex>>> =
231        Lazy::new(|| RwLock::new(HashMap::new()));
232
233    #[test]
234    #[allow(clippy::too_many_lines)]
235    fn test_ip() {
236        let v4 = v4();
237        let v6 = v6();
238        let v4not = v4not();
239        let v6not = v6not();
240        // v4, v6, v4not, v6not must be declared in this module
241        // Build boundary & extract fixtures equivalent to the JS objects:
242        let mut v6boundaries: HashMap<&str, Vec<&str>> = HashMap::new();
243        v6boundaries.insert(
244            "02001:0000:1234:0000:0000:C1C0:ABCD:0876",
245            vec!["2001:0000:1234:0000:0000:C1C0:ABCD:0876"],
246        );
247        v6boundaries.insert(
248            "fe80:0000:0000:0000:0204:61ff:fe9d:f156245",
249            vec!["fe80:0000:0000:0000:0204:61ff:fe9d:f156"],
250        );
251
252        let mut v6extract: HashMap<&str, Vec<&str>> = HashMap::new();
253        v6extract.insert("::1, ::2, ::3-::5", vec!["::1", "::2", "::3", "::5"]);
254        v6extract
255            .insert("::1  ::2 ::3 - ::5", vec!["::1", "::2", "::3", "::5"]);
256        v6extract.insert(
257            "::ffff:192.168.1.1 1::1.2.3.4",
258            vec!["::ffff:192.168.1.1", "1::1.2.3.4"],
259        );
260        v6extract.insert(
261            "02001:0000:1234:0000:0000:C1C0:ABCD:0876 a::xyz",
262            vec!["2001:0000:1234:0000:0000:C1C0:ABCD:0876", "a::"],
263        );
264
265        let mut v4boundaries: HashMap<&str, Vec<&str>> = HashMap::new();
266        v4boundaries.insert("0000000192.168.0.200", vec!["192.168.0.200"]);
267        v4boundaries.insert("192.168.0.2000000000", vec!["192.168.0.200"]);
268
269        let mut v4extract: HashMap<&str, Vec<&str>> = HashMap::new();
270        v4extract.insert(
271            "255.255.255.255 0.0.0.0",
272            vec!["255.255.255.255", "0.0.0.0"],
273        );
274        v4extract.insert(
275            "1.2.3.4, 6.7.8.9, 1.2.3.4-5.6.7.8",
276            vec!["1.2.3.4", "6.7.8.9", "1.2.3.4", "5.6.7.8"],
277        );
278        v4extract.insert(
279            "1.2.3.4 6.7.8.9 1.2.3.4 - 5.6.7.8",
280            vec!["1.2.3.4", "6.7.8.9", "1.2.3.4", "5.6.7.8"],
281        );
282        v4extract.insert("192.168.0.2000000000", vec!["192.168.0.200"]);
283
284        // 1) v4 exact matches
285        for fixture in &v4 {
286            let re = build_regex_rt(
287                &RegexType::V4V6,
288                Some(&Options {
289                    exact: true,
290                    include_boundaries: false,
291                }),
292            );
293            assert!(
294                re.is_match(fixture).expect("Error"),
295                "Expected to match '{}' with regex '{}'",
296                fixture,
297                re.as_str()
298            );
299        }
300
301        // 2) v4 extract from surrounding text
302        for fixture in &v4 {
303            let re = build_regex_rt(&RegexType::V4V6, None);
304            let haystack = format!("foo {fixture} bar");
305            let caps = re.find(&haystack).expect("Error");
306            assert_eq!(
307                caps.map(|m| m.as_str()),
308                Some(fixture.as_str()),
309                "Expected to match '{}' with regex '{}'",
310                fixture,
311                re.as_str()
312            );
313        }
314
315        // 3) v4 inside joined text and includeBoundaries behavior
316        for fixture in &v4 {
317            let re = build_regex_rt(&RegexType::V4V6, None);
318            assert!(re.is_match(&format!("foo{fixture}bar")).expect("Error"));
319
320            let re_bound = build_regex_rt(
321                &RegexType::V4V6,
322                Some(&Options {
323                    exact: false,
324                    include_boundaries: true,
325                }),
326            );
327            assert!(
328                !re_bound
329                    .is_match(&format!("foo{fixture}bar"))
330                    .expect("Error"),
331                "Expected to NOT match '{}' with regex '{}'",
332                fixture,
333                re_bound.as_str()
334            );
335        }
336
337        // 4) v4not exact should not match
338        for fixture in &v4not {
339            let re = build_regex_rt(
340                &RegexType::V4V6,
341                Some(&Options {
342                    exact: true,
343                    include_boundaries: false,
344                }),
345            );
346            assert!(
347                !re.is_match(fixture).expect("Error"),
348                "Expected to NOT match '{}' with regex '{}'",
349                fixture,
350                re.as_str()
351            );
352        }
353
354        // 5) v6 exact matches
355        for fixture in &v6 {
356            let re = build_regex_rt(
357                &RegexType::V4V6,
358                Some(&Options {
359                    exact: true,
360                    include_boundaries: false,
361                }),
362            );
363            assert!(
364                re.is_match(fixture).expect("Error"),
365                "Expected to match '{}' with regex '{}'",
366                fixture,
367                re.as_str()
368            );
369        }
370
371        // 6) v6 extract from surrounding text
372        for fixture in &v6 {
373            let re = build_regex_rt(&RegexType::V4V6, None);
374            let haystack = format!("foo {fixture} bar");
375            let caps = re.find(&haystack).expect("Error");
376            assert_eq!(
377                caps.map(|m| m.as_str()),
378                Some(fixture.as_str()),
379                "Expected to match '{}' with regex '{}'",
380                fixture,
381                re.as_str()
382            );
383        }
384
385        // 7) v6 inside joined text and includeBoundaries behavior
386        for fixture in &v6 {
387            let re = build_regex_rt(&RegexType::V4V6, None);
388            assert!(
389                re.is_match(&format!("foo{fixture}bar")).expect("Error"),
390                "Expected to match '{}' with regex '{}'",
391                fixture,
392                re.as_str()
393            );
394
395            let re_bound = build_regex_rt(
396                &RegexType::V4V6,
397                Some(&Options {
398                    exact: false,
399                    include_boundaries: true,
400                }),
401            );
402            assert!(
403                !re_bound
404                    .is_match(&format!("foo{fixture}bar"))
405                    .expect("Error"),
406                "Expected to NOT match '{}' with regex '{}'",
407                fixture,
408                re.as_str()
409            );
410        }
411
412        // 8) v6not exact should not match
413        for fixture in &v6not {
414            let re = build_regex_rt(
415                &RegexType::V4V6,
416                Some(&Options {
417                    exact: true,
418                    include_boundaries: false,
419                }),
420            );
421            assert!(
422                !re.is_match(fixture).expect("Error"),
423                "Expected to NOT match '{}' with regex '{}'",
424                fixture,
425                re.as_str()
426            );
427        }
428
429        // 9) v4 boundaries/extraction behavior
430        for (fixture, expected) in &v4boundaries {
431            let re = build_regex_rt(&RegexType::V4, None);
432            assert!(
433                re.is_match(fixture).expect("Error"),
434                "Expected to match '{}' with regex '{}'",
435                fixture,
436                re.as_str()
437            );
438
439            // collect all matches
440            let matches: Vec<&str> = re
441                .find(fixture)
442                .expect("Error")
443                .map(|m| m.as_str())
444                .into_iter()
445                .collect();
446            assert_eq!(
447                &matches,
448                expected,
449                "Expected to match '{}' with regex '{}'",
450                fixture,
451                re.as_str()
452            );
453
454            let re_bound = build_regex_rt(
455                &RegexType::V4,
456                Some(&Options {
457                    exact: false,
458                    include_boundaries: true,
459                }),
460            );
461            assert!(!re_bound.is_match(fixture).expect("Error"));
462            let matches2: Vec<&str> = re_bound
463                .find(fixture)
464                .expect("Error")
465                .map(|m| m.as_str())
466                .into_iter()
467                .collect();
468            assert!(
469                matches2.is_empty(),
470                "Expected to find no matches for '{}' with regex '{}'",
471                fixture,
472                re_bound.as_str()
473            );
474        }
475
476        for (fixture, expected) in &v4extract {
477            let re = build_regex_rt(&RegexType::V4, None);
478            let matches: Vec<&str> = re
479                .find_iter(fixture)
480                .map(|m| m.expect("Error").as_str())
481                .collect();
482            assert_eq!(
483                &matches,
484                expected,
485                "Expected to match '{}' with regex '{}'",
486                fixture,
487                re.as_str()
488            );
489        }
490
491        // 10) v6 boundaries/extraction behavior
492        for (fixture, expected) in &v6boundaries {
493            let re = build_regex_rt(&RegexType::V6, None);
494            assert!(re.is_match(fixture).expect("Error"));
495            let matches: Vec<&str> = re
496                .find(fixture)
497                .expect("Error")
498                .map(|m| m.as_str())
499                .into_iter()
500                .collect();
501            assert_eq!(
502                &matches,
503                expected,
504                "Expected to match '{}' with regex '{}'",
505                fixture,
506                re.as_str()
507            );
508
509            let re_bound = build_regex_rt(
510                &RegexType::V6,
511                Some(&Options {
512                    exact: false,
513                    include_boundaries: true,
514                }),
515            );
516            assert!(
517                !re_bound.is_match(fixture).expect("Error"),
518                "Expected to NOT match '{}' with regex '{}'",
519                fixture,
520                re_bound.as_str()
521            );
522            let matches2: Vec<&str> = re_bound
523                .find(fixture)
524                .expect("Error")
525                .map(|m| m.as_str())
526                .into_iter()
527                .collect();
528            assert!(
529                matches2.is_empty(),
530                "Expected to find no matches for '{}' with regex '{}'",
531                fixture,
532                re_bound.as_str()
533            );
534        }
535
536        for (fixture, expected) in &v6extract {
537            let re = build_regex_rt(&RegexType::V6, None);
538            let matches: Vec<&str> = re
539                .find_iter(fixture)
540                .map(|m| m.expect("Error").as_str())
541                .collect();
542            assert_eq!(
543                &matches,
544                expected,
545                "Expected to match '{}' with regex '{}'",
546                fixture,
547                re.as_str()
548            );
549        }
550    }
551
552    #[test]
553    #[allow(clippy::too_many_lines)]
554    fn test_ip_v4() {
555        let v4 = v4();
556        let v4not = v4not();
557        // v4 exact
558        for fixture in &v4 {
559            let re = build_regex_rt(
560                &RegexType::V4,
561                Some(&Options {
562                    exact: true,
563                    include_boundaries: false,
564                }),
565            );
566            assert!(re.is_match(fixture).expect("Error"));
567        }
568
569        for fixture in &v4 {
570            let re = build_regex_rt(&RegexType::V4, None);
571            let haystack = format!("foo {fixture} bar");
572            let caps = re.find(&haystack).expect("Error");
573            assert_eq!(
574                caps.map(|m| m.as_str()),
575                Some(fixture.as_str()),
576                "Expected to match '{}' with regex '{}'",
577                fixture,
578                re.as_str()
579            );
580        }
581
582        for fixture in &v4 {
583            let re = build_regex_rt(&RegexType::V4, None);
584            assert!(
585                re.is_match(&format!("foo{fixture}bar")).expect("Error"),
586                "Expected to match '{}' with regex '{}'",
587                fixture,
588                re.as_str()
589            );
590
591            let re_bound = build_regex_rt(
592                &RegexType::V4,
593                Some(&Options {
594                    exact: false,
595                    include_boundaries: true,
596                }),
597            );
598            assert!(
599                !re_bound
600                    .is_match(&format!("foo{fixture}bar"))
601                    .expect("Error"),
602                "Expected to match '{}' with regex '{}'",
603                fixture,
604                re.as_str()
605            );
606        }
607
608        for fixture in &v4not {
609            let re = build_regex_rt(
610                &RegexType::V4,
611                Some(&Options {
612                    exact: true,
613                    include_boundaries: false,
614                }),
615            );
616            assert!(!re.is_match(fixture).expect("Error"));
617        }
618
619        // v4 boundaries/extract
620        let mut v4boundaries: HashMap<&str, Vec<&str>> = HashMap::new();
621        v4boundaries.insert("0000000192.168.0.200", vec!["192.168.0.200"]);
622        v4boundaries.insert("192.168.0.2000000000", vec!["192.168.0.200"]);
623
624        let mut v4extract: HashMap<&str, Vec<&str>> = HashMap::new();
625        v4extract.insert(
626            "255.255.255.255 0.0.0.0",
627            vec!["255.255.255.255", "0.0.0.0"],
628        );
629        v4extract.insert(
630            "1.2.3.4, 6.7.8.9, 1.2.3.4-5.6.7.8",
631            vec!["1.2.3.4", "6.7.8.9", "1.2.3.4", "5.6.7.8"],
632        );
633        v4extract.insert(
634            "1.2.3.4 6.7.8.9 1.2.3.4 - 5.6.7.8",
635            vec!["1.2.3.4", "6.7.8.9", "1.2.3.4", "5.6.7.8"],
636        );
637        v4extract.insert("192.168.0.2000000000", vec!["192.168.0.200"]);
638
639        for (fixture, expected) in &v4boundaries {
640            let re = build_regex_rt(&RegexType::V4, None);
641            assert!(re.is_match(fixture).expect("Error"));
642            let matches: Vec<&str> = re
643                .find(fixture)
644                .expect("Error")
645                .map(|m| m.as_str())
646                .into_iter()
647                .collect();
648            assert_eq!(&matches, expected);
649
650            let re_bound = build_regex_rt(
651                &RegexType::V4,
652                Some(&Options {
653                    exact: false,
654                    include_boundaries: true,
655                }),
656            );
657            assert!(!re_bound.is_match(fixture).expect("Error"));
658            let matches2: Vec<&str> = re_bound
659                .find(fixture)
660                .expect("Error")
661                .map(|m| m.as_str())
662                .into_iter()
663                .collect();
664            assert!(matches2.is_empty());
665        }
666
667        for (fixture, expected) in &v4extract {
668            let re = build_regex_rt(&RegexType::V4, None);
669            let matches: Vec<&str> = re
670                .find_iter(fixture)
671                .map(|m| m.expect("Error").as_str())
672                .collect();
673            assert_eq!(&matches, expected);
674        }
675    }
676
677    #[test]
678    #[allow(clippy::too_many_lines)]
679    fn test_ip_v6() {
680        let v6 = v6();
681        let v6not = v6not();
682        // v6 exact
683        for fixture in &v6 {
684            let re = build_regex_rt(
685                &RegexType::V6,
686                Some(&Options {
687                    exact: true,
688                    include_boundaries: false,
689                }),
690            );
691            assert!(re.is_match(fixture).expect("Error"));
692        }
693
694        for fixture in &v6 {
695            let re = build_regex_rt(&RegexType::V6, None);
696            let haystack = format!("foo {fixture} bar");
697            let caps = re.find(&haystack).expect("Error");
698            assert_eq!(caps.map(|m| m.as_str()), Some(fixture.as_str()));
699        }
700
701        for fixture in &v6 {
702            let re = build_regex_rt(&RegexType::V6, None);
703            assert!(re.is_match(&format!("foo{fixture}bar")).expect("Error"));
704
705            let re_bound = build_regex_rt(
706                &RegexType::V6,
707                Some(&Options {
708                    exact: false,
709                    include_boundaries: true,
710                }),
711            );
712            assert!(
713                !re_bound
714                    .is_match(&format!("foo{fixture}bar"))
715                    .expect("Error")
716            );
717        }
718
719        for fixture in &v6not {
720            let re = build_regex_rt(
721                &RegexType::V6,
722                Some(&Options {
723                    exact: true,
724                    include_boundaries: false,
725                }),
726            );
727            assert!(!re.is_match(fixture).expect("Error"));
728        }
729
730        // v6 boundaries/extract
731        let mut v6boundaries: HashMap<&str, Vec<&str>> = HashMap::new();
732        v6boundaries.insert(
733            "02001:0000:1234:0000:0000:C1C0:ABCD:0876",
734            vec!["2001:0000:1234:0000:0000:C1C0:ABCD:0876"],
735        );
736        v6boundaries.insert(
737            "fe80:0000:0000:0000:0204:61ff:fe9d:f156245",
738            vec!["fe80:0000:0000:0000:0204:61ff:fe9d:f156"],
739        );
740
741        let mut v6extract: HashMap<&str, Vec<&str>> = HashMap::new();
742        v6extract.insert("::1, ::2, ::3-::5", vec!["::1", "::2", "::3", "::5"]);
743        v6extract
744            .insert("::1  ::2 ::3 - ::5", vec!["::1", "::2", "::3", "::5"]);
745        v6extract.insert(
746            "::ffff:192.168.1.1 1::1.2.3.4",
747            vec!["::ffff:192.168.1.1", "1::1.2.3.4"],
748        );
749        v6extract.insert(
750            "02001:0000:1234:0000:0000:C1C0:ABCD:0876 a::xyz",
751            vec!["2001:0000:1234:0000:0000:C1C0:ABCD:0876", "a::"],
752        );
753
754        for (fixture, expected) in &v6boundaries {
755            let re = build_regex_rt(&RegexType::V6, None);
756            assert!(re.is_match(fixture).expect("Error"));
757            let matches: Vec<&str> = re
758                .find(fixture)
759                .expect("Error")
760                .map(|m| m.as_str())
761                .into_iter()
762                .collect();
763            assert_eq!(
764                &matches,
765                expected,
766                "Expected to match '{}' with regex '{}'",
767                fixture,
768                re.as_str()
769            );
770
771            let re_bound = build_regex_rt(
772                &RegexType::V6,
773                Some(&Options {
774                    exact: false,
775                    include_boundaries: true,
776                }),
777            );
778            assert!(
779                !re_bound.is_match(fixture).expect("Error"),
780                "Expected to NOT match '{}' with regex '{}'",
781                fixture,
782                re_bound.as_str()
783            );
784            let matches2: Vec<&str> = re_bound
785                .find(fixture)
786                .expect("Error")
787                .map(|m| m.as_str())
788                .into_iter()
789                .collect();
790            assert!(
791                matches2.is_empty(),
792                "Expected to find no matches for '{}' with regex '{}'",
793                fixture,
794                re_bound.as_str()
795            );
796        }
797
798        for (fixture, expected) in &v6extract {
799            let re = build_regex_rt(&RegexType::V6, None);
800            let matches: Vec<&str> = re
801                .find_iter(fixture)
802                .map(|m| m.expect("Error").as_str())
803                .collect();
804            assert_eq!(
805                &matches,
806                expected,
807                "Expected to match '{}' with regex '{}'",
808                fixture,
809                re.as_str()
810            );
811        }
812    }
813
814    #[crate::ctb_test]
815    #[should_panic(
816        expected = "Cannot set both exact and include_boundaries options to true"
817    )]
818    fn test_disallows_both_options() {
819        get_regex(
820            &RegexType::V4V6,
821            Some(&Options {
822                exact: true,
823                include_boundaries: true,
824            }),
825        );
826    }
827
828    #[crate::ctb_test]
829    fn test_compatible() {
830        // Test that the get_regex function produces the same regex strings as the original ip-regex library for various options.
831        let inexact_no_boundaries = Some(&Options {
832            exact: false,
833            include_boundaries: false,
834        });
835        let inexact_boundaries = Some(&Options {
836            exact: false,
837            include_boundaries: true,
838        });
839        let exact_no_boundaries = Some(&Options {
840            exact: true,
841            include_boundaries: false,
842        });
843
844        let both_inexact_no_boundaries =
845            get_regex(&RegexType::V4V6, inexact_no_boundaries);
846        let expected = "(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3})|(?:(?:(?:[a-fA-F\\d]{1,4}:){7}(?:[a-fA-F\\d]{1,4}|:)|(?:[a-fA-F\\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|:[a-fA-F\\d]{1,4}|:)|(?:[a-fA-F\\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,2}|:)|(?:[a-fA-F\\d]{1,4}:){4}(?:(?::[a-fA-F\\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,3}|:)|(?:[a-fA-F\\d]{1,4}:){3}(?:(?::[a-fA-F\\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,4}|:)|(?:[a-fA-F\\d]{1,4}:){2}(?:(?::[a-fA-F\\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,5}|:)|(?:[a-fA-F\\d]{1,4}:){1}(?:(?::[a-fA-F\\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?)";
847        assert_eq!(both_inexact_no_boundaries, expected);
848
849        let both_inexact_boundaries =
850            get_regex(&RegexType::V4V6, inexact_boundaries);
851        let expected = "(?:(?:(?<=\\s|^)(?=[a-fA-F\\d:])|(?<=[a-fA-F\\d:])(?=\\s|$))(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}(?:(?<=\\s|^)(?=[a-fA-F\\d:])|(?<=[a-fA-F\\d:])(?=\\s|$)))|(?:(?:(?<=\\s|^)(?=[a-fA-F\\d:])|(?<=[a-fA-F\\d:])(?=\\s|$))(?:(?:[a-fA-F\\d]{1,4}:){7}(?:[a-fA-F\\d]{1,4}|:)|(?:[a-fA-F\\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|:[a-fA-F\\d]{1,4}|:)|(?:[a-fA-F\\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,2}|:)|(?:[a-fA-F\\d]{1,4}:){4}(?:(?::[a-fA-F\\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,3}|:)|(?:[a-fA-F\\d]{1,4}:){3}(?:(?::[a-fA-F\\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,4}|:)|(?:[a-fA-F\\d]{1,4}:){2}(?:(?::[a-fA-F\\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,5}|:)|(?:[a-fA-F\\d]{1,4}:){1}(?:(?::[a-fA-F\\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?(?:(?<=\\s|^)(?=[a-fA-F\\d:])|(?<=[a-fA-F\\d:])(?=\\s|$)))";
852        assert_eq!(both_inexact_boundaries, expected);
853
854        let both_exact_no_boundaries =
855            get_regex(&RegexType::V4V6, exact_no_boundaries);
856        let expected = "(?:^(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}$)|(?:^(?:(?:[a-fA-F\\d]{1,4}:){7}(?:[a-fA-F\\d]{1,4}|:)|(?:[a-fA-F\\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|:[a-fA-F\\d]{1,4}|:)|(?:[a-fA-F\\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,2}|:)|(?:[a-fA-F\\d]{1,4}:){4}(?:(?::[a-fA-F\\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,3}|:)|(?:[a-fA-F\\d]{1,4}:){3}(?:(?::[a-fA-F\\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,4}|:)|(?:[a-fA-F\\d]{1,4}:){2}(?:(?::[a-fA-F\\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,5}|:)|(?:[a-fA-F\\d]{1,4}:){1}(?:(?::[a-fA-F\\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?$)";
857        assert_eq!(both_exact_no_boundaries, expected);
858
859        let v4_inexact_no_boundaries =
860            get_regex(&RegexType::V4, inexact_no_boundaries);
861        let expected = "(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}";
862        assert_eq!(v4_inexact_no_boundaries, expected);
863
864        let v4_inexact_boundaries =
865            get_regex(&RegexType::V4, inexact_boundaries);
866        let expected = "(?:(?<=\\s|^)(?=[a-fA-F\\d:])|(?<=[a-fA-F\\d:])(?=\\s|$))(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}(?:(?<=\\s|^)(?=[a-fA-F\\d:])|(?<=[a-fA-F\\d:])(?=\\s|$))";
867        assert_eq!(v4_inexact_boundaries, expected);
868
869        let v4_exact_no_boundaries =
870            get_regex(&RegexType::V4, exact_no_boundaries);
871        let expected = "^(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}$";
872        assert_eq!(v4_exact_no_boundaries, expected);
873
874        let v6_inexact_no_boundaries =
875            get_regex(&RegexType::V6, inexact_no_boundaries);
876        let expected = "(?:(?:[a-fA-F\\d]{1,4}:){7}(?:[a-fA-F\\d]{1,4}|:)|(?:[a-fA-F\\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|:[a-fA-F\\d]{1,4}|:)|(?:[a-fA-F\\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,2}|:)|(?:[a-fA-F\\d]{1,4}:){4}(?:(?::[a-fA-F\\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,3}|:)|(?:[a-fA-F\\d]{1,4}:){3}(?:(?::[a-fA-F\\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,4}|:)|(?:[a-fA-F\\d]{1,4}:){2}(?:(?::[a-fA-F\\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,5}|:)|(?:[a-fA-F\\d]{1,4}:){1}(?:(?::[a-fA-F\\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?";
877        assert_eq!(v6_inexact_no_boundaries, expected);
878
879        let v6_inexact_boundaries =
880            get_regex(&RegexType::V6, inexact_boundaries);
881        let expected = "(?:(?<=\\s|^)(?=[a-fA-F\\d:])|(?<=[a-fA-F\\d:])(?=\\s|$))(?:(?:[a-fA-F\\d]{1,4}:){7}(?:[a-fA-F\\d]{1,4}|:)|(?:[a-fA-F\\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|:[a-fA-F\\d]{1,4}|:)|(?:[a-fA-F\\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,2}|:)|(?:[a-fA-F\\d]{1,4}:){4}(?:(?::[a-fA-F\\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,3}|:)|(?:[a-fA-F\\d]{1,4}:){3}(?:(?::[a-fA-F\\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,4}|:)|(?:[a-fA-F\\d]{1,4}:){2}(?:(?::[a-fA-F\\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,5}|:)|(?:[a-fA-F\\d]{1,4}:){1}(?:(?::[a-fA-F\\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?(?:(?<=\\s|^)(?=[a-fA-F\\d:])|(?<=[a-fA-F\\d:])(?=\\s|$))";
882        assert_eq!(v6_inexact_boundaries, expected);
883
884        let v6_exact_no_boundaries =
885            get_regex(&RegexType::V6, exact_no_boundaries);
886        let expected = "^(?:(?:[a-fA-F\\d]{1,4}:){7}(?:[a-fA-F\\d]{1,4}|:)|(?:[a-fA-F\\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|:[a-fA-F\\d]{1,4}|:)|(?:[a-fA-F\\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,2}|:)|(?:[a-fA-F\\d]{1,4}:){4}(?:(?::[a-fA-F\\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,3}|:)|(?:[a-fA-F\\d]{1,4}:){3}(?:(?::[a-fA-F\\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,4}|:)|(?:[a-fA-F\\d]{1,4}:){2}(?:(?::[a-fA-F\\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,5}|:)|(?:[a-fA-F\\d]{1,4}:){1}(?:(?::[a-fA-F\\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?$";
887        assert_eq!(v6_exact_no_boundaries, expected);
888    }
889
890    fn v4() -> Vec<String> {
891        let vec = vec![
892            "0.0.0.0",
893            "8.8.8.8",
894            "127.0.0.1",
895            "100.100.100.100",
896            "192.168.0.1",
897            "18.101.25.153",
898            "123.23.34.2",
899            "172.26.168.134",
900            "212.58.241.131",
901            "128.0.0.0",
902            "23.71.254.72",
903            "223.255.255.255",
904            "192.0.2.235",
905            "99.198.122.146",
906            "46.51.197.88",
907            "173.194.34.134",
908        ];
909
910        vec.into_iter().map(String::from).collect()
911    }
912
913    fn v4not() -> Vec<String> {
914        let vec = vec![
915            ".100.100.100.100",
916            "100..100.100.100.",
917            "100.100.100.100.",
918            "999.999.999.999",
919            "256.256.256.256",
920            "256.100.100.100.100",
921            "123.123.123",
922            "http://123.123.123",
923            "1000.2.3.4",
924            "999.2.3.4",
925        ];
926
927        vec.into_iter().map(String::from).collect()
928    }
929
930    #[allow(clippy::too_many_lines)]
931    fn v6() -> Vec<String> {
932        let vec = vec![
933            "::",
934            "1::",
935            "::1",
936            "1::8",
937            "1::7:8",
938            "1:2:3:4:5:6:7:8",
939            "1:2:3:4:5:6::8",
940            "1:2:3:4:5:6:7::",
941            "1:2:3:4:5::7:8",
942            "1:2:3:4:5::8",
943            "1:2:3::8",
944            "1::4:5:6:7:8",
945            "1::6:7:8",
946            "1::3:4:5:6:7:8",
947            "1:2:3:4::6:7:8",
948            "1:2::4:5:6:7:8",
949            "::2:3:4:5:6:7:8",
950            "1:2::8",
951            "2001:0000:1234:0000:0000:C1C0:ABCD:0876",
952            "3ffe:0b00:0000:0000:0001:0000:0000:000a",
953            "FF02:0000:0000:0000:0000:0000:0000:0001",
954            "0000:0000:0000:0000:0000:0000:0000:0001",
955            "0000:0000:0000:0000:0000:0000:0000:0000",
956            "::ffff:192.168.1.26",
957            "2::10",
958            "ff02::1",
959            "fe80::",
960            "2002::",
961            "2001:db8::",
962            "2001:0db8:1234::",
963            "::ffff:0:0",
964            "::ffff:192.168.1.1",
965            "1:2:3:4::8",
966            "1::2:3:4:5:6:7",
967            "1::2:3:4:5:6",
968            "1::2:3:4:5",
969            "1::2:3:4",
970            "1::2:3",
971            "::2:3:4:5:6:7",
972            "::2:3:4:5:6",
973            "::2:3:4:5",
974            "::2:3:4",
975            "::2:3",
976            "::8",
977            "1:2:3:4:5:6::",
978            "1:2:3:4:5::",
979            "1:2:3:4::",
980            "1:2:3::",
981            "1:2::",
982            "1:2:3:4::7:8",
983            "1:2:3::7:8",
984            "1:2::7:8",
985            "1:2:3:4:5:6:1.2.3.4",
986            "1:2:3:4:5::1.2.3.4",
987            "1:2:3:4::1.2.3.4",
988            "1:2:3::1.2.3.4",
989            "1:2::1.2.3.4",
990            "1::1.2.3.4",
991            "1:2:3:4::5:1.2.3.4",
992            "1:2:3::5:1.2.3.4",
993            "1:2::5:1.2.3.4",
994            "1::5:1.2.3.4",
995            "1::5:11.22.33.44",
996            "fe80::217:f2ff:254.7.237.98",
997            "fe80::217:f2ff:fe07:ed62",
998            "2001:DB8:0:0:8:800:200C:417A",
999            "FF01:0:0:0:0:0:0:101",
1000            "0:0:0:0:0:0:0:1",
1001            "0:0:0:0:0:0:0:0",
1002            "2001:DB8::8:800:200C:417A",
1003            "FF01::101",
1004            "0:0:0:0:0:0:13.1.68.3",
1005            "0:0:0:0:0:FFFF:129.144.52.38",
1006            "::13.1.68.3",
1007            "::FFFF:129.144.52.38",
1008            "fe80:0000:0000:0000:0204:61ff:fe9d:f156",
1009            "fe80:0:0:0:204:61ff:fe9d:f156",
1010            "fe80::204:61ff:fe9d:f156",
1011            "fe80:0:0:0:204:61ff:254.157.241.86",
1012            "fe80::204:61ff:254.157.241.86",
1013            "fe80::1",
1014            "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
1015            "2001:db8:85a3:0:0:8a2e:370:7334",
1016            "2001:db8:85a3::8a2e:370:7334",
1017            "2001:0db8:0000:0000:0000:0000:1428:57ab",
1018            "2001:0db8:0000:0000:0000::1428:57ab",
1019            "2001:0db8:0:0:0:0:1428:57ab",
1020            "2001:0db8:0:0::1428:57ab",
1021            "2001:0db8::1428:57ab",
1022            "2001:db8::1428:57ab",
1023            "::ffff:12.34.56.78",
1024            "::ffff:0c22:384e",
1025            "2001:0db8:1234:0000:0000:0000:0000:0000",
1026            "2001:0db8:1234:ffff:ffff:ffff:ffff:ffff",
1027            "2001:db8:a::123",
1028            "::ffff:192.0.2.128",
1029            "::ffff:c000:280",
1030            "a:b:c:d:e:f:f1:f2",
1031            "a:b:c::d:e:f:f1",
1032            "a:b:c::d:e:f",
1033            "a:b:c::d:e",
1034            "a:b:c::d",
1035            "::a",
1036            "::a:b:c",
1037            "::a:b:c:d:e:f:f1",
1038            "a::",
1039            "a:b:c::",
1040            "a:b:c:d:e:f:f1::",
1041            "a:bb:ccc:dddd:000e:00f:0f::",
1042            "0:a:0:a:0:0:0:a",
1043            "0:a:0:0:a:0:0:a",
1044            "2001:db8:1:1:1:1:0:0",
1045            "2001:db8:1:1:1:0:0:0",
1046            "2001:db8:1:1:0:0:0:0",
1047            "2001:db8:1:0:0:0:0:0",
1048            "2001:db8:0:0:0:0:0:0",
1049            "2001:0:0:0:0:0:0:0",
1050            "A:BB:CCC:DDDD:000E:00F:0F::",
1051            "0:0:0:0:0:0:0:a",
1052            "0:0:0:0:a:0:0:0",
1053            "0:0:0:a:0:0:0:0",
1054            "a:0:0:a:0:0:a:a",
1055            "a:0:0:a:0:0:0:a",
1056            "a:0:0:0:a:0:0:a",
1057            "a:0:0:0:a:0:0:0",
1058            "a:0:0:0:0:0:0:0",
1059            "fe80::7:8%eth0",
1060            "fe80::7:8%1",
1061        ];
1062
1063        vec.into_iter().map(String::from).collect()
1064    }
1065
1066    fn v6not() -> Vec<String> {
1067        let vec = vec![
1068            "",
1069            "1:",
1070            ":1",
1071            "11:36:12",
1072            "02001:0000:1234:0000:0000:C1C0:ABCD:0876",
1073            "2001:0000:1234:0000:00001:C1C0:ABCD:0876",
1074            "2001:0000:1234: 0000:0000:C1C0:ABCD:0876",
1075            "2001:1:1:1:1:1:255Z255X255Y255",
1076            "3ffe:0b00:0000:0001:0000:0000:000a",
1077            "FF02:0000:0000:0000:0000:0000:0000:0000:0001",
1078            "3ffe:b00::1::a",
1079            "::1111:2222:3333:4444:5555:6666::",
1080            "1:2:3::4:5::7:8",
1081            "12345::6:7:8",
1082            "1::5:400.2.3.4",
1083            "1::5:260.2.3.4",
1084            "1::5:256.2.3.4",
1085            "1::5:1.256.3.4",
1086            "1::5:1.2.256.4",
1087            "1::5:1.2.3.256",
1088            "1::5:300.2.3.4",
1089            "1::5:1.300.3.4",
1090            "1::5:1.2.300.4",
1091            "1::5:1.2.3.300",
1092            "1::5:900.2.3.4",
1093            "1::5:1.900.3.4",
1094            "1::5:1.2.900.4",
1095            "1::5:1.2.3.900",
1096            "1::5:300.300.300.300",
1097            "1::5:3000.30.30.30",
1098            "1::400.2.3.4",
1099            "1::260.2.3.4",
1100            "1::256.2.3.4",
1101            "1::1.256.3.4",
1102            "1::1.2.256.4",
1103            "1::1.2.3.256",
1104            "1::300.2.3.4",
1105            "1::1.300.3.4",
1106            "1::1.2.300.4",
1107            "1::1.2.3.300",
1108            "1::900.2.3.4",
1109            "1::1.900.3.4",
1110            "1::1.2.900.4",
1111            "1::1.2.3.900",
1112            "1::300.300.300.300",
1113            "1::3000.30.30.30",
1114            "::400.2.3.4",
1115            "::260.2.3.4",
1116            "::256.2.3.4",
1117            "::1.256.3.4",
1118            "::1.2.256.4",
1119            "::1.2.3.256",
1120            "::300.2.3.4",
1121            "::1.300.3.4",
1122            "::1.2.300.4",
1123            "::1.2.3.300",
1124            "::900.2.3.4",
1125            "::1.900.3.4",
1126            "::1.2.900.4",
1127            "::1.2.3.900",
1128            "::300.300.300.300",
1129            "::3000.30.30.30",
1130            "2001:DB8:0:0:8:800:200C:417A:221",
1131            "FF01::101::2",
1132            "1111:2222:3333:4444::5555:",
1133            "1111:2222:3333::5555:",
1134            "1111:2222::5555:",
1135            "1111::5555:",
1136            "::5555:",
1137            ":::",
1138            "1111:",
1139            ":",
1140            ":1111:2222:3333:4444::5555",
1141            ":1111:2222:3333::5555",
1142            ":1111:2222::5555",
1143            ":1111::5555",
1144            ":::5555",
1145            "1.2.3.4:1111:2222:3333:4444::5555",
1146            "1.2.3.4:1111:2222:3333::5555",
1147            "1.2.3.4:1111:2222::5555",
1148            "1.2.3.4:1111::5555",
1149            "1.2.3.4::5555",
1150            "1.2.3.4::",
1151            "fe80:0000:0000:0000:0204:61ff:254.157.241.086",
1152            "123",
1153            "ldkfj",
1154            "2001::FFD3::57ab",
1155            "2001:db8:85a3::8a2e:37023:7334",
1156            "2001:db8:85a3::8a2e:370k:7334",
1157            "1:2:3:4:5:6:7:8:9",
1158            "1::2::3",
1159            "1:::3:4:5",
1160            "1:2:3::4:5:6:7:8:9",
1161            "::ffff:2.3.4",
1162            "::ffff:257.1.2.3",
1163            "::ffff:12345678901234567890.1.26",
1164        ];
1165
1166        vec.into_iter().map(String::from).collect()
1167    }
1168}