from typing import List, Optional, Sequence from .net_ipv4_parse_ipv4 import ipv4_value _ALLOWED = frozenset("0123456789abcdefABCDEF:.") def parse_ipv6(text: str) -> List[int]: """The eight 16-bit groups of an IPv6 address written as RFC 4291 allows. Hex groups of one to four digits in either case, at most one "::" standing for one or more zero groups, and optionally a dotted IPv4 address as the last 32 bits. A zone id ("%eth0") or a prefix ("/64") is not part of an address and is refused, as is anything around it (spaces, newlines). """ groups = ipv6_groups(text) if groups is None: raise ValueError('"%s" is not an IPv6 address' % (text,)) return groups # Exported for is_ipv6 and for capabilities that parse IPv6 text without # wanting an error; not part of the group's contract. def ipv6_groups(text: object) -> Optional[List[int]]: """The eight groups, or None when the text is not an IPv6 address.""" if not isinstance(text, str) or text == "": return None for ch in text: if ch not in _ALLOWED: return None at = text.find("::") if at < 0: groups = _pieces(text.split(":"), True) return groups if groups is not None and len(groups) == 8 else None before = text[:at] after = text[at + 2:] if "::" in after: return None head = _pieces(before.split(":") if before else [], False) tail = _pieces(after.split(":") if after else [], True) if head is None or tail is None: return None # "::" stands for at least one group, so at most seven are written. if len(head) + len(tail) > 7: return None return head + [0] * (8 - len(head) - len(tail)) + tail def _pieces(parts: Sequence[str], may_end_with_ipv4: bool) -> Optional[List[int]]: groups: List[int] = [] for i, part in enumerate(parts): if "." in part: if not may_end_with_ipv4 or i != len(parts) - 1: return None v4 = ipv4_value(part) if v4 is None: return None groups.extend([v4 // 65536, v4 % 65536]) continue if len(part) < 1 or len(part) > 4: return None groups.append(int(part, 16)) return groups