is_ip/
lib.rs

1//! Check if a string is an IP address.
2
3use std::sync::OnceLock;
4
5use regex::Regex;
6
7const MAX_IPV4_LENGTH: usize = 15;
8const MAX_IPV6_LENGTH: usize = 45;
9
10const V4: &str = r#"(?: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}"#;
11
12fn v6() -> &'static str {
13    static PATTERN: OnceLock<String> = OnceLock::new();
14    PATTERN.get_or_init(|| {
15        const V6_SEG: &str = r#"[a-fA-F\d]{1,4}"#;
16        #[rustfmt::skip]
17        let buf = [
18            "(?:", 
19            "(?:", V6_SEG, ":){7}(?:", V6_SEG, "|:)|",                                            // 1:2:3:4:5:6:7::  1:2:3:4:5:6:7:8
20            "(?:", V6_SEG, ":){6}(?:", V4, "|:", V6_SEG, "|:)|",                                  // 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
21            "(?:", V6_SEG, ":){5}(?::", V4, "|(?::", V6_SEG, "){1,2}|:)|",                        // 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
22            "(?:", V6_SEG, ":){4}(?:(?::", V6_SEG, "){0,1}:", V4, "|(?::", V6_SEG, "){1,3}|:)|",  // 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
23            "(?:", V6_SEG, ":){3}(?:(?::", V6_SEG, "){0,2}:", V4, "|(?::", V6_SEG, "){1,4}|:)|",  // 1:2:3::          1:2:3::5:6:7:8   1:2:3::8        1:2:3::5:6:7:1.2.3.4
24            "(?:", V6_SEG, ":){2}(?:(?::", V6_SEG, "){0,3}:", V4, "|(?::", V6_SEG, "){1,5}|:)|",  // 1:2::            1:2::4:5:6:7:8   1:2::8          1:2::4:5:6:7:1.2.3.4
25            "(?:", V6_SEG, ":){1}(?:(?::", V6_SEG, "){0,4}:", V4, "|(?::", V6_SEG, "){1,6}|:)|",  // 1::              1::3:4:5:6:7:8   1::8            1::3:4:5:6:7:1.2.3.4
26            "(?::(?:(?::", V6_SEG, "){0,5}:", V4, "|(?::", V6_SEG, "){1,7}|:))",                  // ::2:3:4:5:6:7:8  ::2:3:4:5:6:7:8  ::8             ::1.2.3.4
27            ")(?:%[0-9a-zA-Z]{1,})?"                                                              // %eth0            %1
28        ];
29        buf.join("").to_owned()
30    })
31}
32
33fn ip_regex() -> &'static Regex {
34    static REGEX: OnceLock<Regex> = OnceLock::new();
35    REGEX.get_or_init(|| Regex::new(format!("(?:^{}$)|(?:^{}$)", V4, v6()).as_str()).unwrap())
36}
37
38fn ipv4_regex() -> &'static Regex {
39    static REGEX: OnceLock<Regex> = OnceLock::new();
40    REGEX.get_or_init(|| Regex::new(format!("^{}$", V4).as_str()).unwrap())
41}
42
43fn ipv6_regex() -> &'static Regex {
44    static REGEX: OnceLock<Regex> = OnceLock::new();
45    REGEX.get_or_init(|| Regex::new(format!("^{}$", v6()).as_str()).unwrap())
46}
47
48/// Check if `string` is IPv6 or IPv4.
49pub fn is_ip(string: &str) -> bool {
50    if string.len() > MAX_IPV6_LENGTH {
51        return false;
52    }
53
54    ip_regex().is_match(string)
55}
56
57/// Check if `string` is IPv4.
58pub fn is_ipv4(string: &str) -> bool {
59    if string.len() > MAX_IPV4_LENGTH {
60        return false;
61    }
62
63    ipv4_regex().is_match(string)
64}
65
66/// Check if `string` is IPv6.
67pub fn is_ipv6(string: &str) -> bool {
68    if string.len() > MAX_IPV6_LENGTH {
69        return false;
70    }
71
72    ipv6_regex().is_match(string)
73}
74
75/// Returns `Some(6)` if `string` is IPv6, `Some(4)` if `string` is IPv4,
76/// or `None` if `string` is neither.
77pub fn ip_version(string: &str) -> Option<u8> {
78    if is_ipv6(string) {
79        return Some(6);
80    }
81
82    if is_ipv4(string) {
83        return Some(4);
84    }
85
86    None
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    const TEST_V4: [&str; 16] = [
94        "0.0.0.0",
95        "8.8.8.8",
96        "127.0.0.1",
97        "100.100.100.100",
98        "192.168.0.1",
99        "18.101.25.153",
100        "123.23.34.2",
101        "172.26.168.134",
102        "212.58.241.131",
103        "128.0.0.0",
104        "23.71.254.72",
105        "223.255.255.255",
106        "192.0.2.235",
107        "99.198.122.146",
108        "46.51.197.88",
109        "173.194.34.134",
110    ];
111
112    const TEST_V4NOT: [&str; 10] = [
113        ".100.100.100.100",
114        "100..100.100.100.",
115        "100.100.100.100.",
116        "999.999.999.999",
117        "256.256.256.256",
118        "256.100.100.100.100",
119        "123.123.123",
120        "http://123.123.123",
121        "1000.2.3.4",
122        "999.2.3.4",
123    ];
124
125    const TEST_V6: [&str; 128] = [
126        "::",
127        "1::",
128        "::1",
129        "1::8",
130        "1::7:8",
131        "1:2:3:4:5:6:7:8",
132        "1:2:3:4:5:6::8",
133        "1:2:3:4:5:6:7::",
134        "1:2:3:4:5::7:8",
135        "1:2:3:4:5::8",
136        "1:2:3::8",
137        "1::4:5:6:7:8",
138        "1::6:7:8",
139        "1::3:4:5:6:7:8",
140        "1:2:3:4::6:7:8",
141        "1:2::4:5:6:7:8",
142        "::2:3:4:5:6:7:8",
143        "1:2::8",
144        "2001:0000:1234:0000:0000:C1C0:ABCD:0876",
145        "3ffe:0b00:0000:0000:0001:0000:0000:000a",
146        "FF02:0000:0000:0000:0000:0000:0000:0001",
147        "0000:0000:0000:0000:0000:0000:0000:0001",
148        "0000:0000:0000:0000:0000:0000:0000:0000",
149        "::ffff:192.168.1.26",
150        "2::10",
151        "ff02::1",
152        "fe80::",
153        "2002::",
154        "2001:db8::",
155        "2001:0db8:1234::",
156        "::ffff:0:0",
157        "::ffff:192.168.1.1",
158        "1:2:3:4::8",
159        "1::2:3:4:5:6:7",
160        "1::2:3:4:5:6",
161        "1::2:3:4:5",
162        "1::2:3:4",
163        "1::2:3",
164        "::2:3:4:5:6:7",
165        "::2:3:4:5:6",
166        "::2:3:4:5",
167        "::2:3:4",
168        "::2:3",
169        "::8",
170        "1:2:3:4:5:6::",
171        "1:2:3:4:5::",
172        "1:2:3:4::",
173        "1:2:3::",
174        "1:2::",
175        "1:2:3:4::7:8",
176        "1:2:3::7:8",
177        "1:2::7:8",
178        "1:2:3:4:5:6:1.2.3.4",
179        "1:2:3:4:5::1.2.3.4",
180        "1:2:3:4::1.2.3.4",
181        "1:2:3::1.2.3.4",
182        "1:2::1.2.3.4",
183        "1::1.2.3.4",
184        "1:2:3:4::5:1.2.3.4",
185        "1:2:3::5:1.2.3.4",
186        "1:2::5:1.2.3.4",
187        "1::5:1.2.3.4",
188        "1::5:11.22.33.44",
189        "fe80::217:f2ff:254.7.237.98",
190        "fe80::217:f2ff:fe07:ed62",
191        "2001:DB8:0:0:8:800:200C:417A",
192        "FF01:0:0:0:0:0:0:101",
193        "0:0:0:0:0:0:0:1",
194        "0:0:0:0:0:0:0:0",
195        "2001:DB8::8:800:200C:417A",
196        "FF01::101",
197        "0:0:0:0:0:0:13.1.68.3",
198        "0:0:0:0:0:FFFF:129.144.52.38",
199        "::13.1.68.3",
200        "::FFFF:129.144.52.38",
201        "fe80:0000:0000:0000:0204:61ff:fe9d:f156",
202        "fe80:0:0:0:204:61ff:fe9d:f156",
203        "fe80::204:61ff:fe9d:f156",
204        "fe80:0:0:0:204:61ff:254.157.241.86",
205        "fe80::204:61ff:254.157.241.86",
206        "fe80::1",
207        "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
208        "2001:db8:85a3:0:0:8a2e:370:7334",
209        "2001:db8:85a3::8a2e:370:7334",
210        "2001:0db8:0000:0000:0000:0000:1428:57ab",
211        "2001:0db8:0000:0000:0000::1428:57ab",
212        "2001:0db8:0:0:0:0:1428:57ab",
213        "2001:0db8:0:0::1428:57ab",
214        "2001:0db8::1428:57ab",
215        "2001:db8::1428:57ab",
216        "::ffff:12.34.56.78",
217        "::ffff:0c22:384e",
218        "2001:0db8:1234:0000:0000:0000:0000:0000",
219        "2001:0db8:1234:ffff:ffff:ffff:ffff:ffff",
220        "2001:db8:a::123",
221        "::ffff:192.0.2.128",
222        "::ffff:c000:280",
223        "a:b:c:d:e:f:f1:f2",
224        "a:b:c::d:e:f:f1",
225        "a:b:c::d:e:f",
226        "a:b:c::d:e",
227        "a:b:c::d",
228        "::a",
229        "::a:b:c",
230        "::a:b:c:d:e:f:f1",
231        "a::",
232        "a:b:c::",
233        "a:b:c:d:e:f:f1::",
234        "a:bb:ccc:dddd:000e:00f:0f::",
235        "0:a:0:a:0:0:0:a",
236        "0:a:0:0:a:0:0:a",
237        "2001:db8:1:1:1:1:0:0",
238        "2001:db8:1:1:1:0:0:0",
239        "2001:db8:1:1:0:0:0:0",
240        "2001:db8:1:0:0:0:0:0",
241        "2001:db8:0:0:0:0:0:0",
242        "2001:0:0:0:0:0:0:0",
243        "A:BB:CCC:DDDD:000E:00F:0F::",
244        "0:0:0:0:0:0:0:a",
245        "0:0:0:0:a:0:0:0",
246        "0:0:0:a:0:0:0:0",
247        "a:0:0:a:0:0:a:a",
248        "a:0:0:a:0:0:0:a",
249        "a:0:0:0:a:0:0:a",
250        "a:0:0:0:a:0:0:0",
251        "a:0:0:0:0:0:0:0",
252        "fe80::7:8%eth0",
253        "fe80::7:8%1",
254    ];
255
256    const TEST_V6NOT: [&str; 96] = [
257        "",
258        "1:",
259        ":1",
260        "11:36:12",
261        "02001:0000:1234:0000:0000:C1C0:ABCD:0876",
262        "2001:0000:1234:0000:00001:C1C0:ABCD:0876",
263        "2001:0000:1234: 0000:0000:C1C0:ABCD:0876",
264        "2001:1:1:1:1:1:255Z255X255Y255",
265        "3ffe:0b00:0000:0001:0000:0000:000a",
266        "FF02:0000:0000:0000:0000:0000:0000:0000:0001",
267        "3ffe:b00::1::a",
268        "::1111:2222:3333:4444:5555:6666::",
269        "1:2:3::4:5::7:8",
270        "12345::6:7:8",
271        "1::5:400.2.3.4",
272        "1::5:260.2.3.4",
273        "1::5:256.2.3.4",
274        "1::5:1.256.3.4",
275        "1::5:1.2.256.4",
276        "1::5:1.2.3.256",
277        "1::5:300.2.3.4",
278        "1::5:1.300.3.4",
279        "1::5:1.2.300.4",
280        "1::5:1.2.3.300",
281        "1::5:900.2.3.4",
282        "1::5:1.900.3.4",
283        "1::5:1.2.900.4",
284        "1::5:1.2.3.900",
285        "1::5:300.300.300.300",
286        "1::5:3000.30.30.30",
287        "1::400.2.3.4",
288        "1::260.2.3.4",
289        "1::256.2.3.4",
290        "1::1.256.3.4",
291        "1::1.2.256.4",
292        "1::1.2.3.256",
293        "1::300.2.3.4",
294        "1::1.300.3.4",
295        "1::1.2.300.4",
296        "1::1.2.3.300",
297        "1::900.2.3.4",
298        "1::1.900.3.4",
299        "1::1.2.900.4",
300        "1::1.2.3.900",
301        "1::300.300.300.300",
302        "1::3000.30.30.30",
303        "::400.2.3.4",
304        "::260.2.3.4",
305        "::256.2.3.4",
306        "::1.256.3.4",
307        "::1.2.256.4",
308        "::1.2.3.256",
309        "::300.2.3.4",
310        "::1.300.3.4",
311        "::1.2.300.4",
312        "::1.2.3.300",
313        "::900.2.3.4",
314        "::1.900.3.4",
315        "::1.2.900.4",
316        "::1.2.3.900",
317        "::300.300.300.300",
318        "::3000.30.30.30",
319        "2001:DB8:0:0:8:800:200C:417A:221",
320        "FF01::101::2",
321        "1111:2222:3333:4444::5555:",
322        "1111:2222:3333::5555:",
323        "1111:2222::5555:",
324        "1111::5555:",
325        "::5555:",
326        ":::",
327        "1111:",
328        ":",
329        ":1111:2222:3333:4444::5555",
330        ":1111:2222:3333::5555",
331        ":1111:2222::5555",
332        ":1111::5555",
333        ":::5555",
334        "1.2.3.4:1111:2222:3333:4444::5555",
335        "1.2.3.4:1111:2222:3333::5555",
336        "1.2.3.4:1111:2222::5555",
337        "1.2.3.4:1111::5555",
338        "1.2.3.4::5555",
339        "1.2.3.4::",
340        "fe80:0000:0000:0000:0204:61ff:254.157.241.086",
341        "123",
342        "ldkfj",
343        "2001::FFD3::57ab",
344        "2001:db8:85a3::8a2e:37023:7334",
345        "2001:db8:85a3::8a2e:370k:7334",
346        "1:2:3:4:5:6:7:8:9",
347        "1::2::3",
348        "1:::3:4:5",
349        "1:2:3::4:5:6:7:8:9",
350        "::ffff:2.3.4",
351        "::ffff:257.1.2.3",
352        "::ffff:12345678901234567890.1.26",
353    ];
354
355    #[test]
356    fn test_ip_regex() {
357        for fixture in TEST_V4 {
358            assert_eq!(ip_regex().is_match(fixture), true);
359        }
360
361        for fixture in TEST_V4NOT {
362            assert_eq!(ip_regex().is_match(fixture), false);
363        }
364
365        for fixture in TEST_V6 {
366            assert_eq!(ip_regex().is_match(fixture), true);
367        }
368
369        for fixture in TEST_V6NOT {
370            assert_eq!(ip_regex().is_match(fixture), false);
371        }
372    }
373
374    #[test]
375    fn test_ipv4_regex() {
376        for fixture in TEST_V4 {
377            assert_eq!(ipv4_regex().is_match(fixture), true);
378        }
379
380        for fixture in TEST_V4NOT {
381            assert_eq!(ipv4_regex().is_match(fixture), false);
382        }
383    }
384
385    #[test]
386    fn test_ipv6_regex() {
387        for fixture in TEST_V6 {
388            assert_eq!(ipv6_regex().is_match(fixture), true);
389        }
390
391        for fixture in TEST_V6NOT {
392            assert_eq!(ipv6_regex().is_match(fixture), false);
393        }
394    }
395
396    #[test]
397    fn test_ip() {
398        assert_eq!(is_ip("192.168.0.1"), true);
399        assert_eq!(is_ip("1:2:3:4:5:6:7:8"), true);
400        assert_eq!(is_ip("::1"), true);
401        assert_eq!(is_ip("2001:0dc5:72a3:0000:0000:802e:3370:73E4"), true);
402    }
403
404    #[test]
405    fn test_ipv4() {
406        assert_eq!(is_ipv4("192.168.0.1"), true);
407        assert_eq!(is_ipv4("123.123.123.1234"), false);
408        assert_eq!(is_ipv4("123.123.123.12345"), false);
409    }
410
411    #[test]
412    fn test_ipv6() {
413        assert_eq!(is_ipv6("1:2:3:4:5:6:7:8"), true);
414        assert_eq!(is_ipv6("::1"), true);
415        assert_eq!(is_ipv6("0000:0000:0000:0000:0000:0000:0000:0000"), true);
416        assert_eq!(is_ipv6("0000:0000:0000:0000:0000:ffff:192.168.100.228"), true);
417        assert_eq!(is_ipv6("2001:0dc5:72a3:0000:0000:802e:3370:73E4"), true);
418        assert_eq!(is_ipv6("2001:0dc5:72a3:0000:0000:802e:3370:73E4XXXXXXXX"), false);
419    }
420
421    #[test]
422    fn test_ip_version() {
423        assert_eq!(ip_version("192.168.0.1"), Some(4));
424        assert_eq!(ip_version("1:2:3:4:5:6:7:8"), Some(6));
425        assert_eq!(ip_version("abc"), None);
426        assert_eq!(ip_version("123.123.123.12345"), None);
427    }
428}