Skip to main content

iptools/ipv6/
mod.rs

1// Copyright (c) 2025 Denis Avvakumov
2// Licensed under the MIT license,  https://opensource.org/licenses/MIT
3
4use core::fmt;
5#[cfg(feature = "std")]
6use lazy_regex::regex;
7
8use crate::error::Error;
9use crate::error::Result;
10
11#[cfg(feature = "std")]
12static HEX_RE: &lazy_regex::Lazy<lazy_regex::Regex> =
13    regex!(r"^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$");
14
15#[cfg(feature = "std")]
16static DOTTED_QUAD_RE: &lazy_regex::Lazy<lazy_regex::Regex> =
17    regex!(r"^([0-9a-fA-F]{0,4}:){2,6}(\d{1,3}\.){3}\d{1,3}$");
18
19// Kept for compatibility with earlier public API
20#[allow(dead_code)]
21#[cfg(feature = "std")]
22static RE_RFC1924: &lazy_regex::Lazy<lazy_regex::Regex> =
23    regex!(r"^[0-9A-Za-z!#$%&()*+-;<=>?@^_`{|}~]{20}$");
24
25/// Last ip
26pub const MAX_IP: u128 = u128::MAX;
27
28/// First ip
29pub const MIN_IP: u128 = 0;
30
31/// IETF and IANA reserved ip addresses
32pub const RESERVED_RANGES: &[&str] = &[
33    UNSPECIFIED_ADDRESS,
34    LOOPBACK,
35    IPV4_MAPPED,
36    IPV6_TO_IPV4_NETWORK,
37    TEREDO_NETWORK,
38    PRIVATE_NETWORK,
39    LINK_LOCAL,
40    MULTICAST,
41    MULTICAST_LOOPBACK,
42    MULTICAST_LOCAL,
43    MULTICAST_SITE,
44    MULTICAST_SITE_ORG,
45    MULTICAST_GLOBAL,
46    MULTICAST_LOCAL_NODES,
47    MULTICAST_LOCAL_ROUTERS,
48    MULTICAST_LOCAL_DHCP,
49    MULTICAST_SITE_DHCP,
50];
51
52/// Absence of an address (only valid as source address)
53/// [RFC 4291](https://tools.ietf.org/html/rfc4291)
54pub const UNSPECIFIED_ADDRESS: &str = "::/128";
55
56/// Loopback addresses on the local host
57/// [RFC 4291](https://tools.ietf.org/html/rfc4291)
58pub const LOOPBACK: &str = "::1/128";
59
60/// Common `localhost` address
61/// [RFC 4291](https://tools.ietf.org/html/rfc4291)
62pub const LOCALHOST: &str = LOOPBACK;
63
64/// IPv4 mapped to IPv6 (not globally routable)
65/// [RFC 4291](https://tools.ietf.org/html/rfc4291)
66pub const IPV4_MAPPED: &str = "::ffff:0:0/96";
67
68/// Documentation and example network
69/// [RFC 3849](https://tools.ietf.org/html/rfc3849)
70pub const DOCUMENTATION_NETWORK: &str = "2001:db8::/32";
71
72/// 6to4 Address block
73/// [RFC 3056](https://tools.ietf.org/html/rfc3056)
74pub const IPV6_TO_IPV4_NETWORK: &str = "2002::/16";
75
76/// Teredo addresses
77/// [RFC 4380](https://tools.ietf.org/html/rfc4380)
78pub const TEREDO_NETWORK: &str = "2001::/32";
79
80/// Private network
81/// [RFC 4193](https://tools.ietf.org/html/rfc4193)
82pub const PRIVATE_NETWORK: &str = "fd00::/8";
83
84/// Link-Local unicast networks (not globally routable)
85/// [RFC 4291](https://tools.ietf.org/html/rfc4291)
86pub const LINK_LOCAL: &str = "fe80::/10";
87
88/// Multicast reserved block
89/// [RFC 4291](https://tools.ietf.org/html/rfc4291)
90pub const MULTICAST: &str = "ff00::/8";
91
92/// Interface-Local multicast
93pub const MULTICAST_LOOPBACK: &str = "ff01::/16";
94
95/// Link-Local multicast
96pub const MULTICAST_LOCAL: &str = "ff02::/16";
97
98/// Site-Local multicast
99pub const MULTICAST_SITE: &str = "ff05::/16";
100
101/// Organization-Local multicast
102pub const MULTICAST_SITE_ORG: &str = "ff08::/16";
103
104/// Global multicast
105pub const MULTICAST_GLOBAL: &str = "ff0e::/16";
106
107/// All nodes on the local segment
108pub const MULTICAST_LOCAL_NODES: &str = "ff02::1";
109
110/// All routers on the local segment
111pub const MULTICAST_LOCAL_ROUTERS: &str = "ff02::2";
112
113/// All DHCP servers and relay agents on the local segment
114pub const MULTICAST_LOCAL_DHCP: &str = "ff02::1:2";
115
116/// All DHCP servers and relay agents on the local site
117pub const MULTICAST_SITE_DHCP: &str = "ff05::1:3";
118
119const RFC1924_ALPHABET_BYTES: &[u8; 85] =
120    b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~";
121
122const fn build_rfc1924_rev_table() -> [i8; 128] {
123    let mut table = [-1i8; 128];
124    let mut i = 0;
125    while i < RFC1924_ALPHABET_BYTES.len() {
126        let byte = RFC1924_ALPHABET_BYTES[i] as usize;
127        if byte < 128 {
128            table[byte] = i as i8;
129        }
130        i += 1;
131    }
132    table
133}
134
135const RFC1924_REV_TABLE: [i8; 128] = build_rfc1924_rev_table();
136
137#[inline]
138fn push_generated_ascii(out: &mut alloc::string::String, bytes: &[u8]) {
139    match core::str::from_utf8(bytes) {
140        Ok(text) => out.push_str(text),
141        Err(_) => push_generated_ascii_slow(out, bytes),
142    }
143}
144
145#[cold]
146fn push_generated_ascii_slow(out: &mut alloc::string::String, bytes: &[u8]) {
147    for &byte in bytes {
148        out.push(char::from(byte));
149    }
150}
151
152#[inline]
153fn generated_ascii_string(bytes: &[u8]) -> alloc::string::String {
154    match core::str::from_utf8(bytes) {
155        Ok(text) => alloc::string::String::from(text),
156        Err(_) => {
157            let mut out = alloc::string::String::with_capacity(bytes.len());
158            push_generated_ascii_slow(&mut out, bytes);
159            out
160        }
161    }
162}
163
164#[inline]
165fn generated_ascii_vec_string(bytes: alloc::vec::Vec<u8>) -> alloc::string::String {
166    match alloc::string::String::from_utf8(bytes) {
167        Ok(text) => text,
168        Err(err) => {
169            let bytes = err.into_bytes();
170            generated_ascii_string(&bytes)
171        }
172    }
173}
174
175#[inline]
176fn fmt_generated_ascii(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
177    match core::str::from_utf8(bytes) {
178        Ok(text) => f.write_str(text),
179        Err(_) => fmt_generated_ascii_slow(f, bytes),
180    }
181}
182
183#[cold]
184fn fmt_generated_ascii_slow(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
185    for &byte in bytes {
186        fmt::Write::write_char(f, char::from(byte))?;
187    }
188    Ok(())
189}
190
191enum Ipv6HextetParseError {
192    DottedQuadTail,
193    Invalid(Error),
194}
195
196/// Validate a hexadecimal IPV6 ip address using regex
197///
198/// Note: This function applies regex pre-filtering and then parses with
199/// `ip2long()`. For better performance, consider using `validate_ip()`.
200///
201/// # Example
202///
203/// ```
204/// use iptools::ipv6::validate_ip_re;
205/// assert_eq!(validate_ip_re("::ffff:192.0.2.300"), false);
206/// assert_eq!(validate_ip_re("1080:0:0:0:8:800:200c:417a"), true);
207/// ```
208#[cfg(feature = "std")]
209pub fn validate_ip_re(ip: &str) -> bool {
210    let is_hex = HEX_RE.is_match(ip);
211    let is_dotted_quad = DOTTED_QUAD_RE.is_match(ip);
212    (is_hex || is_dotted_quad) && ip2long(ip).is_ok()
213}
214
215/// Validate a hexadecimal IPV6 ip address (optimized)
216///
217/// This function uses the optimized `ip2long` parser for validation,
218/// making it significantly faster than `validate_ip_re()` which uses regex.
219///
220/// Per [RFC 4291 2.2](https://datatracker.ietf.org/doc/html/rfc4291#section-2.2)
221/// we reject syntactically ambiguous forms such as `:::1`. The original Python
222/// `iptools` library still accepts that input, so you can verify the difference
223/// locally with:
224/// ```python
225/// >>> from iptools import ipv6
226/// >>> ipv6.validate_ip(':::1')
227/// True
228/// ```
229///
230/// # Example
231///
232/// ```
233/// use iptools::ipv6::validate_ip;
234/// assert_eq!(validate_ip("::ffff:192.0.2.300"), false);
235/// assert_eq!(validate_ip("1080:0:0:0:8:800:200c:417a"), true);
236/// ```
237pub fn validate_ip(ip: &str) -> bool {
238    ip2long(ip).is_ok()
239}
240
241/// Convert a hexadecimal IPV6 address to a network byte order 128 bit integer
242///
243/// # Example
244///
245/// ```
246/// use iptools::ipv6::ip2long;
247/// assert_eq!(ip2long("::"), Ok(0));
248/// assert_eq!(ip2long("::1"), Ok(1));
249/// assert_eq!(ip2long("2001:db8:85a3::8a2e:370:7334"),Ok(0x20010db885a3000000008a2e03707334));
250/// ```
251#[inline(always)]
252pub fn ip2long(ip: &str) -> Result<u128> {
253    let bytes = ip.as_bytes();
254    match parse_ipv6_hextets(bytes) {
255        Ok(value) => Ok(value),
256        Err(Ipv6HextetParseError::DottedQuadTail) => parse_ipv6_mixed_dotted_quad(bytes),
257        Err(Ipv6HextetParseError::Invalid(err)) => Err(err),
258    }
259}
260
261#[inline(always)]
262fn parse_ipv6_mixed_dotted_quad(bytes: &[u8]) -> Result<u128> {
263    let Some(pos) = bytes.iter().rposition(|&b| b == b':') else {
264        // Keep IPv6 parser strict on address family: bare IPv4 is not IPv6.
265        return Err(Error::V6IP());
266    };
267
268    let suffix = &bytes[pos + 1..];
269    let (v6_src, v4_suffix) = if suffix.contains(&b'.') {
270        let src = if pos > 0 && bytes[pos - 1] == b':' {
271            &bytes[..pos + 1]
272        } else {
273            &bytes[..pos]
274        };
275        (src, Some(suffix)) // IPv4-mapped like "::ffff:127.0.0.1"
276    } else {
277        // Dot was not in the final segment, let the IPv6 prefix parser reject it.
278        (bytes, None)
279    };
280
281    let mut parts = [0u16; 8];
282    let mut capacity = 8;
283
284    if let Some(suffix) = v4_suffix {
285        if v6_src.is_empty() {
286            return Err(Error::V6IP());
287        }
288        // RFC-oriented dotted tail parser:
289        // x:x:x:x:x:x:d.d.d.d must use exactly four decimal octets.
290        let v4_int = parse_ipv4_dotted_quad_strict(suffix).ok_or(Error::V6IP())?;
291        parts[6] = (v4_int >> 16) as u16;
292        parts[7] = v4_int as u16;
293        capacity = 6;
294    }
295
296    parse_ipv6_prefix(v6_src, &mut parts, capacity)?;
297
298    Ok(groups_to_u128(&parts))
299}
300
301#[inline(always)]
302fn parse_ipv6_hextets(src: &[u8]) -> core::result::Result<u128, Ipv6HextetParseError> {
303    let len = src.len();
304    if len == 0 {
305        return Err(Ipv6HextetParseError::Invalid(Error::V6IP()));
306    }
307
308    let mut idx = 0usize;
309    let mut head = 0u128;
310    let mut tail = 0u128;
311    let mut head_count = 0usize;
312    let mut tail_count = 0usize;
313    let mut compression_seen = false;
314
315    while idx < len {
316        if src[idx] == b':' {
317            if idx + 1 < len && src[idx + 1] == b':' && !compression_seen {
318                compression_seen = true;
319                idx += 2;
320                if idx == len {
321                    break;
322                }
323                continue;
324            }
325            return Err(Ipv6HextetParseError::Invalid(Error::V6IP()));
326        }
327
328        let mut value = 0u16;
329        let mut digits = 0usize;
330        while idx < len {
331            let b = src[idx];
332            if b == b':' {
333                break;
334            }
335            if b == b'.' {
336                return Err(Ipv6HextetParseError::DottedQuadTail);
337            }
338            let Some(nibble) = hex_nibble(b) else {
339                return Err(Ipv6HextetParseError::Invalid(Error::V6IPConvert()));
340            };
341            digits += 1;
342            if digits > 4 {
343                return Err(Ipv6HextetParseError::Invalid(Error::V6IPConvert()));
344            }
345            value = (value << 4) | u16::from(nibble);
346            idx += 1;
347        }
348
349        if digits == 0 {
350            return Err(Ipv6HextetParseError::Invalid(Error::V6IP()));
351        }
352
353        if compression_seen {
354            if tail_count >= 8 {
355                return Err(Ipv6HextetParseError::Invalid(Error::V6IP()));
356            }
357            tail = (tail << 16) | u128::from(value);
358            tail_count += 1;
359        } else {
360            if head_count >= 8 {
361                return Err(Ipv6HextetParseError::Invalid(Error::V6IP()));
362            }
363            head = (head << 16) | u128::from(value);
364            head_count += 1;
365        }
366
367        if idx == len {
368            break;
369        }
370
371        if idx + 1 < len && src[idx + 1] == b':' {
372            if compression_seen {
373                return Err(Ipv6HextetParseError::Invalid(Error::V6IP()));
374            }
375            compression_seen = true;
376            idx += 2;
377            if idx == len {
378                break;
379            }
380        } else {
381            idx += 1;
382            if idx == len {
383                return Err(Ipv6HextetParseError::Invalid(Error::V6IP()));
384            }
385        }
386    }
387
388    if compression_seen {
389        if head_count + tail_count >= 8 {
390            return Err(Ipv6HextetParseError::Invalid(Error::V6IP()));
391        }
392        let high = if head_count == 0 {
393            0
394        } else {
395            head << ((8 - head_count) * 16)
396        };
397        Ok(high | tail)
398    } else {
399        if head_count != 8 {
400            return Err(Ipv6HextetParseError::Invalid(Error::V6IP()));
401        }
402        Ok(head)
403    }
404}
405
406/// Parse IPv6 prefix with compression support
407#[inline(always)]
408fn parse_ipv6_prefix(src: &[u8], parts: &mut [u16; 8], capacity: usize) -> Result<()> {
409    let mut i = 0;
410    let mut head_idx = 0;
411    let mut tail_idx = 0;
412    let mut tail_buf = [0u16; 7];
413    let mut compression_seen = false;
414
415    // Parse head segments (before ::)
416    while i < src.len() {
417        // Look ahead for :: compression marker
418        if i + 1 < src.len() && src[i] == b':' && src[i + 1] == b':' {
419            compression_seen = true;
420            i += 2;
421            break;
422        }
423
424        // Parse a single hex segment
425        let start = i;
426        while i < src.len() && src[i] != b':' {
427            i += 1;
428        }
429
430        if start == i {
431            // Empty segment not part of "::"
432            return Err(Error::V6IP());
433        }
434
435        // Non-empty segment
436        if head_idx >= capacity {
437            return Err(Error::V6IP());
438        }
439        parts[head_idx] = parse_hex_u16(&src[start..i]).ok_or(Error::V6IPConvert())?;
440        head_idx += 1;
441
442        if i < src.len() && src[i] == b':' {
443            i += 1; // Skip colon
444            if i == src.len() {
445                // A single trailing ':' is invalid.
446                return Err(Error::V6IP());
447            }
448        }
449
450        // Check for :: after skipping single colon (handles cases where :: follows a segment)
451        if i < src.len() && src[i] == b':' {
452            compression_seen = true;
453            i += 1;
454            break;
455        }
456    }
457
458    // Parse tail segments (after ::)
459    if compression_seen {
460        while i < src.len() {
461            let start = i;
462            while i < src.len() && src[i] != b':' {
463                i += 1;
464            }
465
466            if start == i {
467                return Err(Error::V6IP()); // No empty segments in tail
468            }
469
470            if tail_idx >= tail_buf.len() {
471                return Err(Error::V6IP());
472            }
473            tail_buf[tail_idx] = parse_hex_u16(&src[start..i]).ok_or(Error::V6IPConvert())?;
474            tail_idx += 1;
475
476            if i < src.len() && src[i] == b':' {
477                i += 1;
478                if i == src.len() {
479                    // A single trailing ':' after tail segment is invalid.
480                    return Err(Error::V6IP());
481                }
482            } else {
483                break;
484            }
485        }
486
487        // Place tail at end of address
488        if head_idx + tail_idx >= capacity {
489            return Err(Error::V6IP());
490        }
491        if tail_idx > 0 {
492            let insert_pos = capacity - tail_idx;
493            parts[insert_pos..capacity].copy_from_slice(&tail_buf[..tail_idx]);
494        }
495        // Gap is already there from array initialization
496    } else if head_idx != capacity {
497        // No compression, must fill exactly capacity segments
498        return Err(Error::V6IP());
499    }
500
501    Ok(())
502}
503
504/// Parse strict dotted-quad IPv4 suffix for IPv6 text forms.
505///
506/// Accepts exactly 4 decimal octets in [0, 255], rejects shorthand
507/// forms and multi-digit leading zeros.
508///
509/// This matches RFC 4291 section 2.2 (`x:x:x:x:x:x:d.d.d.d`) and
510/// RFC 3986 `IPv4address` (`dec-octet "." dec-octet "." dec-octet "." dec-octet`).
511#[inline(always)]
512fn parse_ipv4_dotted_quad_strict(src: &[u8]) -> Option<u32> {
513    let mut octets = [0u32; 4];
514    let mut octet_idx = 0usize;
515    let mut value = 0u32;
516    let mut digits = 0usize;
517
518    for &b in src {
519        match b {
520            b'0'..=b'9' => {
521                if digits == 0 {
522                    value = (b - b'0') as u32;
523                    digits = 1;
524                } else {
525                    if digits == 1 && value == 0 {
526                        // No leading zeros in multi-digit octets.
527                        return None;
528                    }
529                    value = value * 10 + u32::from(b - b'0');
530                    digits += 1;
531                    if digits > 3 || value > 255 {
532                        return None;
533                    }
534                }
535            }
536            b'.' => {
537                if digits == 0 || octet_idx >= 3 {
538                    return None;
539                }
540                octets[octet_idx] = value;
541                octet_idx += 1;
542                value = 0;
543                digits = 0;
544            }
545            _ => return None,
546        }
547    }
548
549    if digits == 0 || octet_idx != 3 {
550        return None;
551    }
552    octets[3] = value;
553
554    Some((octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3])
555}
556
557/// Parse hex string to u16 (0-FFFF)
558#[inline(always)]
559fn parse_hex_u16(src: &[u8]) -> Option<u16> {
560    let len = src.len();
561    if len == 0 || len > 4 {
562        return None;
563    }
564
565    let mut val = 0u16;
566    for &b in src {
567        val <<= 4;
568        val += match b {
569            b'0'..=b'9' => (b - b'0') as u16,
570            b'a'..=b'f' => (b - b'a' + 10) as u16,
571            b'A'..=b'F' => (b - b'A' + 10) as u16,
572            _ => return None,
573        };
574    }
575    Some(val)
576}
577
578#[inline(always)]
579fn hex_nibble(b: u8) -> Option<u8> {
580    match b {
581        b'0'..=b'9' => Some(b - b'0'),
582        b'a'..=b'f' => Some(b - b'a' + 10),
583        b'A'..=b'F' => Some(b - b'A' + 10),
584        _ => None,
585    }
586}
587
588/// Assemble u128 from 8 u16 groups
589#[inline(always)]
590fn groups_to_u128(groups: &[u16; 8]) -> u128 {
591    (u128::from(groups[0]) << 112)
592        | (u128::from(groups[1]) << 96)
593        | (u128::from(groups[2]) << 80)
594        | (u128::from(groups[3]) << 64)
595        | (u128::from(groups[4]) << 48)
596        | (u128::from(groups[5]) << 32)
597        | (u128::from(groups[6]) << 16)
598        | u128::from(groups[7])
599}
600
601/// Convert a network byte order 128 bit integer to a canonical IPV6 address
602///
603/// # Example
604///
605/// ```
606/// use iptools::ipv6::long2ip;
607/// assert_eq!(long2ip(2130706433, false), "::7f00:1".to_string());
608/// assert_eq!(long2ip(42540766411282592856904266426630537217, false),"2001:db8::1:0:0:1".to_string());
609/// ```
610pub fn long2ip(long_ip: u128, rfc1924: bool) -> alloc::string::String {
611    if rfc1924 {
612        return long2rfc1924(long_ip);
613    }
614
615    let mut buf = [0u8; 39];
616    let len = encode_long2ip(long_ip, &mut buf);
617    generated_ascii_vec_string(buf[..len].to_vec())
618}
619
620/// Convert a network byte order 128 bit integer to an rfc1924 IPV6 address
621///
622/// # Example
623///
624/// ```
625/// use iptools::ipv6::long2rfc1924;
626/// use iptools::ipv6::ip2long;
627/// assert_eq!(long2rfc1924(ip2long("1080::8:800:200C:417A").unwrap()),"4)+k&C#VzJ4br>0wv%Yp");
628/// assert_eq!(long2rfc1924(ip2long("::").unwrap()), "00000000000000000000");
629/// ```
630pub fn long2rfc1924(long_ip: u128) -> alloc::string::String {
631    let mut buf = [b'0'; 20];
632    let mut idx = 20;
633    let mut value = long_ip;
634
635    // Fill from the end to avoid reversing
636    while value > 0 {
637        let digit = (value % 85) as usize;
638        value /= 85;
639        idx -= 1;
640        buf[idx] = RFC1924_ALPHABET_BYTES[digit];
641    }
642
643    generated_ascii_vec_string(buf.to_vec())
644}
645
646/// Convert an RFC1924 IPV6 address to a network byte order 128 bit integer
647///
648/// # Example
649///
650/// ```
651/// use iptools::ipv6::rfc19242long;
652/// assert_eq!(rfc19242long("00000000000000000000"), Some(0));
653/// assert_eq!(rfc19242long("4)+k&C#VzJ4br>0wv%Yp"),Some(21932261930451111902915077091070067066));
654/// assert_eq!(rfc19242long("pizza"), None);
655/// ```
656pub fn rfc19242long(s: &str) -> Option<u128> {
657    if s.len() != 20 {
658        return None;
659    }
660
661    let mut acc = 0u128;
662    for b in s.bytes() {
663        if b >= 128 {
664            return None;
665        }
666        let val = RFC1924_REV_TABLE[b as usize];
667        if val < 0 {
668            return None;
669        }
670        acc = acc.checked_mul(85)?.checked_add(val as u128)?;
671    }
672    Some(acc)
673}
674
675/// Validate a CIDR notation ip address using regex
676///
677/// Note: This function uses regex matching. For better performance,
678/// consider using `validate_cidr()` which uses the optimized parser.
679///
680/// # Example
681///
682/// ```
683/// use iptools::ipv6::validate_cidr_re;
684/// assert_eq!(validate_cidr_re("fc00::/7"), true);
685/// assert_eq!(validate_cidr_re("::ffff:0:0/96"), true);
686/// assert_eq!(validate_cidr_re("::"), false);
687/// assert_eq!(validate_cidr_re("::/129"), false);
688/// ```
689#[cfg(feature = "std")]
690pub fn validate_cidr_re(cidr: &str) -> bool {
691    // Find the '/' separator
692    let Some(slash_pos) = cidr.bytes().position(|b| b == b'/') else {
693        return false;
694    };
695
696    let ip_part = &cidr[..slash_pos];
697    let mask_bytes = &cidr.as_bytes()[slash_pos + 1..];
698
699    // Exactly one slash.
700    if mask_bytes.contains(&b'/') {
701        return false;
702    }
703
704    // Validate prefix range (0-128) and IP (using regex validation)
705    parse_prefix_0_128(mask_bytes).is_some() && validate_ip_re(ip_part)
706}
707
708/// Validate a CIDR notation ip address (optimized)
709///
710/// This function uses the optimized `ip2long` parser for validation,
711/// making it significantly faster than `validate_cidr_re()` which uses regex.
712///
713/// # Example
714///
715/// ```
716/// use iptools::ipv6::validate_cidr;
717/// assert_eq!(validate_cidr("fc00::/7"), true);
718/// assert_eq!(validate_cidr("::ffff:0:0/96"), true);
719/// assert_eq!(validate_cidr("::"), false);
720/// assert_eq!(validate_cidr("::/129"), false);
721/// ```
722pub fn validate_cidr(cidr: &str) -> bool {
723    let bytes = cidr.as_bytes();
724
725    // Find the '/' separator
726    let Some(slash_pos) = bytes.iter().position(|&b| b == b'/') else {
727        return false;
728    };
729
730    let ip_part = &cidr[..slash_pos];
731    let mask_start = slash_pos + 1;
732    let mask_bytes = &bytes[mask_start..];
733    if mask_bytes.contains(&b'/') {
734        return false;
735    }
736
737    // Validate IP (using ip2long for fast validation)
738    parse_prefix_0_128(mask_bytes).is_some() && ip2long(ip_part).is_ok()
739}
740
741/// Convert a CIDR notation ip address into a tuple containing the network block start and end addresses
742///
743/// # Example
744///
745/// ```
746/// use iptools::ipv6::cidr2block;
747/// assert_eq!(cidr2block("2001:db8::/48"),
748///           Ok(("2001:db8::".to_string(), "2001:db8:0:ffff:ffff:ffff:ffff:ffff".to_string())));
749/// assert_eq!(cidr2block("::/0"),
750///           Ok(("::".to_string(), "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff".to_string())));
751/// ```
752pub fn cidr2block(cidr: &str) -> Result<(alloc::string::String, alloc::string::String)> {
753    let (start, end) = cidr_bounds(cidr)?;
754    Ok((long2ip(start, false), long2ip(end, false)))
755}
756
757/// Convert a CIDR notation IPv6 address into raw numeric start/end bounds.
758///
759/// This is the allocation-free counterpart to [`cidr2block`]. Use it when
760/// callers need numeric bounds for range checks or storage and do not need
761/// canonical IPv6 strings.
762///
763/// # Example
764///
765/// ```
766/// use iptools::ipv6::cidr_bounds;
767/// assert_eq!(cidr_bounds("2001:db8::/126").unwrap().1, 0x20010db8000000000000000000000003);
768/// ```
769#[inline(always)]
770pub fn cidr_bounds(cidr: &str) -> Result<(u128, u128)> {
771    let Some(idx) = cidr.find('/') else {
772        return Err(Error::V6CIDR());
773    };
774
775    let ip_str = &cidr[..idx];
776    let prefix_bytes = &cidr.as_bytes()[idx + 1..];
777    if prefix_bytes.contains(&b'/') {
778        return Err(Error::V6CIDR());
779    }
780    let Some(prefix) = parse_prefix_0_128(prefix_bytes) else {
781        return Err(Error::V6CIDR());
782    };
783
784    let ip = ip2long(ip_str)?;
785    block_bounds(ip, prefix)
786}
787
788/// Convert a raw IPv6 address and CIDR prefix into raw numeric start/end bounds.
789///
790/// # Example
791///
792/// ```
793/// use iptools::ipv6::{block_bounds, ip2long};
794/// let ip = ip2long("2001:db8::1234").unwrap();
795/// assert_eq!(block_bounds(ip, 32).unwrap().0, ip2long("2001:db8::").unwrap());
796/// ```
797#[inline(always)]
798pub fn block_bounds(ip: u128, prefix: u8) -> Result<(u128, u128)> {
799    if prefix > 128 {
800        return Err(Error::V6CIDR());
801    }
802    Ok(block_from_ip_and_prefix_raw(ip, prefix))
803}
804
805fn block_from_ip_and_prefix_raw(ip: u128, prefix: u8) -> (u128, u128) {
806    let shift = 128 - u32::from(prefix);
807    if shift == 128 {
808        return (0, u128::MAX);
809    }
810    let block_start = ip & (u128::MAX << shift);
811    (block_start, block_start | ((1u128 << shift) - 1))
812}
813
814fn zero_run_bounds(hextets: &[u16; 8]) -> (usize, usize) {
815    let mut best = (8usize, 0usize);
816    let mut curr_start = 8usize;
817    let mut curr_len = 0usize;
818
819    for (i, &h) in hextets.iter().enumerate() {
820        if h == 0 {
821            if curr_start == 8 {
822                curr_start = i;
823            }
824            curr_len += 1;
825        } else {
826            if curr_len > best.1 {
827                best = (curr_start, curr_len);
828            }
829            curr_start = 8;
830            curr_len = 0;
831        }
832    }
833
834    if curr_len > best.1 {
835        best = (curr_start, curr_len);
836    }
837
838    if best.1 < 2 { (8, 0) } else { best }
839}
840
841fn write_hextet(buf: &mut [u8; 39], len: usize, val: u16) -> usize {
842    const HEX: &[u8; 16] = b"0123456789abcdef";
843
844    if val >= 0x1000 {
845        buf[len] = HEX[(val >> 12) as usize];
846        buf[len + 1] = HEX[((val >> 8) & 0xF) as usize];
847        buf[len + 2] = HEX[((val >> 4) & 0xF) as usize];
848        buf[len + 3] = HEX[(val & 0xF) as usize];
849        len + 4
850    } else if val >= 0x100 {
851        buf[len] = HEX[((val >> 8) & 0xF) as usize];
852        buf[len + 1] = HEX[((val >> 4) & 0xF) as usize];
853        buf[len + 2] = HEX[(val & 0xF) as usize];
854        len + 3
855    } else if val >= 0x10 {
856        buf[len] = HEX[((val >> 4) & 0xF) as usize];
857        buf[len + 1] = HEX[(val & 0xF) as usize];
858        len + 2
859    } else {
860        buf[len] = HEX[val as usize];
861        len + 1
862    }
863}
864
865fn encode_long2ip(long_ip: u128, buf: &mut [u8; 39]) -> usize {
866    let hextets = [
867        (long_ip >> 112) as u16,
868        (long_ip >> 96) as u16,
869        (long_ip >> 80) as u16,
870        (long_ip >> 64) as u16,
871        (long_ip >> 48) as u16,
872        (long_ip >> 32) as u16,
873        (long_ip >> 16) as u16,
874        long_ip as u16,
875    ];
876    let (best_start, best_len) = zero_run_bounds(&hextets);
877
878    let mut len = 0usize;
879    let mut i = 0usize;
880    while i < 8 {
881        if i == best_start {
882            buf[len] = b':';
883            buf[len + 1] = b':';
884            len += 2;
885            i += best_len;
886            continue;
887        }
888
889        if len > 0 && buf[len - 1] != b':' {
890            buf[len] = b':';
891            len += 1;
892        }
893
894        len = write_hextet(buf, len, hextets[i]);
895        i += 1;
896    }
897
898    len
899}
900
901pub(crate) fn push_long2ip(out: &mut alloc::string::String, long_ip: u128) {
902    let mut buf = [0u8; 39];
903    let len = encode_long2ip(long_ip, &mut buf);
904    push_generated_ascii(out, &buf[..len]);
905}
906
907pub(crate) fn fmt_long2ip(f: &mut fmt::Formatter<'_>, long_ip: u128) -> fmt::Result {
908    let mut buf = [0u8; 39];
909    let len = encode_long2ip(long_ip, &mut buf);
910    fmt_generated_ascii(f, &buf[..len])
911}
912
913#[inline(always)]
914fn parse_prefix_0_128(bytes: &[u8]) -> Option<u8> {
915    if bytes.is_empty() {
916        return None;
917    }
918    let mut value: u16 = 0;
919    for &b in bytes {
920        if !b.is_ascii_digit() {
921            return None;
922        }
923        value = value * 10 + u16::from(b - b'0');
924        if value > 128 {
925            return None;
926        }
927    }
928    Some(value as u8)
929}
930
931#[cfg(test)]
932mod tests;