diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py index 20f33cb..4617172 100644 --- a/Lib/ipaddress.py +++ b/Lib/ipaddress.py @@ -499,38 +499,30 @@ class _IPAddressBase: return prefixlen @classmethod - def _prefix_from_ip_string(cls, ip_str): - """Turn a netmask/hostmask string into a prefix length + def _split_addr_prefix(cls, arg): + """Make a (address, prefix) tuple from the given argument. Args: - ip_str: The netmask/hostmask to be converted + arg: A string, bytes object or a tuple representing an address and + a network prefix. Returns: - An integer, the prefix length. - - Raises: - NetmaskValueError: If the input is not a valid netmask/hostmask + A (address, netmask/prefix) tuple. """ - # Parse the netmask/hostmask like an IP address. - try: - ip_int = cls._ip_int_from_string(ip_str) - except AddressValueError: - cls._report_invalid_netmask(ip_str) + if isinstance(arg, (int, bytes)): + addr = arg + prefix = cls._max_prefixlen + elif isinstance(arg, tuple) and len(arg) == 2: + addr, prefix = arg + else: + arg = _split_optional_netmask(arg) + addr = arg[0] + if len(arg) == 2: + prefix = arg[1] + else: + prefix = cls._max_prefixlen - # Try matching a netmask (this would be /1*0*/ as a bitwise regexp). - # Note that the two ambiguous cases (all-ones and all-zeroes) are - # treated as netmasks. - try: - return cls._prefix_from_ip_int(ip_int) - except ValueError: - pass - - # Invert the bits, and try matching a /0+1+/ hostmask instead. - ip_int ^= cls._ALL_ONES - try: - return cls._prefix_from_ip_int(ip_int) - except ValueError: - cls._report_invalid_netmask(ip_str) + return addr, prefix def __reduce__(self): return self.__class__, (str(self),) @@ -1061,9 +1053,6 @@ class _BaseV4: _ALL_ONES = (2**IPV4LENGTH) - 1 _DECIMAL_DIGITS = frozenset('0123456789') - # the valid octets for host and netmasks. only useful for IPv4. - _valid_mask_octets = frozenset({255, 254, 252, 248, 240, 224, 192, 128, 0}) - _max_prefixlen = IPV4LENGTH # There are only a handful of valid v4 netmasks, so we cache them all # when constructed (see _make_netmask()). @@ -1079,20 +1068,41 @@ class _BaseV4: Argument can be: - an integer (the prefix length) - a string representing the prefix length (e.g. "24") - - a string representing the prefix netmask (e.g. "255.255.255.0") + - a string representing the netmask or hostmask (e.g. "255.255.255.0") + - a bytes object representing a netmask or hostmask + (e.g. b"\xff\xff\xff\x00") + - an IPv4Address object representing the netmask or hostmask """ if arg not in cls._netmask_cache: if isinstance(arg, int): prefixlen = arg + netmask = IPv4Address(cls._ip_int_from_prefix(prefixlen)) else: try: + if isinstance(arg, IPv4Address): + netmask = arg + else: + netmask = IPv4Address(arg) + except AddressValueError: # Check for a netmask in prefix length form prefixlen = cls._prefix_from_prefix_string(arg) - except NetmaskValueError: - # Check for a netmask or hostmask in dotted-quad form. - # This may raise NetmaskValueError. - prefixlen = cls._prefix_from_ip_string(arg) - netmask = IPv4Address(cls._ip_int_from_prefix(prefixlen)) + netmask = IPv4Address(cls._ip_int_from_prefix(prefixlen)) + else: + try: + # Try matching a netmask (this would be /1*0*/ as a + # bitwise regexp). Note that the two ambiguous cases + # (all-ones and all-zeroes) are treated as netmasks. + prefixlen = cls._prefix_from_ip_int(netmask._ip) + except ValueError: + # Invert the bits, and try matching a /0+1+/ hostmask + # instead. + ip_int = netmask._ip ^ cls._ALL_ONES + try: + prefixlen = cls._prefix_from_ip_int(ip_int) + except ValueError: + cls._report_invalid_netmask(arg) + else: + netmask = IPv4Address(ip_int) cls._netmask_cache[arg] = netmask, prefixlen return cls._netmask_cache[arg] @@ -1172,57 +1182,23 @@ class _BaseV4: """ return '.'.join(map(str, ip_int.to_bytes(4, 'big'))) - def _is_valid_netmask(self, netmask): - """Verify that the netmask is valid. - - Args: - netmask: A string, either a prefix or dotted decimal - netmask. - - Returns: - A boolean, True if the prefix represents a valid IPv4 - netmask. - - """ - mask = netmask.split('.') - if len(mask) == 4: - try: - for x in mask: - if int(x) not in self._valid_mask_octets: - return False - except ValueError: - # Found something that isn't an integer or isn't valid - return False - for idx, y in enumerate(mask): - if idx > 0 and y > mask[idx - 1]: - return False - return True - try: - netmask = int(netmask) - except ValueError: - return False - return 0 <= netmask <= self._max_prefixlen - - def _is_hostmask(self, ip_str): - """Test if the IP string is a hostmask (rather than a netmask). + @classmethod + def _get_addr_prefix_tuple(cls, arg): + """Make a (address, (netmask, prefix)) tuple from the given argument. Args: - ip_str: A string, the potential hostmask. + arg: A string, bytes object or a tuple representing an address and + a network prefix. Returns: - A boolean, True if the IP string is a hostmask. - + A (address, (netmask, prefix)) tuple. """ - bits = ip_str.split('.') - try: - parts = [x for x in map(int, bits) if x in self._valid_mask_octets] - except ValueError: - return False - if len(parts) != len(bits): - return False - if parts[0] < parts[-1]: - return True - return False + addr, prefix = cls._split_addr_prefix(arg) + if isinstance(addr, IPv4Address): + addr = IPv4Address(addr._ip) + else: + addr = IPv4Address(addr) + return (IPv4Address(addr), cls._make_netmask(prefix)) def _reverse_pointer(self): """Return the reverse DNS pointer name for the IPv4 address. @@ -1362,30 +1338,11 @@ class IPv4Address(_BaseV4, _BaseAddress): class IPv4Interface(IPv4Address): def __init__(self, address): - if isinstance(address, (bytes, int)): - IPv4Address.__init__(self, address) - self.network = IPv4Network(self._ip) - self._prefixlen = self._max_prefixlen - return - - if isinstance(address, tuple): - IPv4Address.__init__(self, address[0]) - if len(address) > 1: - self._prefixlen = int(address[1]) - else: - self._prefixlen = self._max_prefixlen - - self.network = IPv4Network(address, strict=False) - self.netmask = self.network.netmask - self.hostmask = self.network.hostmask - return - - addr = _split_optional_netmask(address) - IPv4Address.__init__(self, addr[0]) - - self.network = IPv4Network(address, strict=False) - self._prefixlen = self.network._prefixlen + addr_prefix = self._get_addr_prefix_tuple(address) + addr, (self.netmask, self._prefixlen) = addr_prefix + super().__init__(addr._ip) + self.network = IPv4Network((addr, self._prefixlen), strict=False) self.netmask = self.network.netmask self.hostmask = self.network.hostmask @@ -1493,49 +1450,18 @@ class IPv4Network(_BaseV4, _BaseNetwork): supplied. """ - _BaseNetwork.__init__(self, address) - - # Constructing from a packed address or integer - if isinstance(address, (int, bytes)): - self.network_address = IPv4Address(address) - self.netmask, self._prefixlen = self._make_netmask(self._max_prefixlen) - #fixme: address/network test here. - return - - if isinstance(address, tuple): - if len(address) > 1: - arg = address[1] - else: - # We weren't given an address[1] - arg = self._max_prefixlen - self.network_address = IPv4Address(address[0]) - self.netmask, self._prefixlen = self._make_netmask(arg) - packed = int(self.network_address) - if packed & int(self.netmask) != packed: - if strict: - raise ValueError('%s has host bits set' % self) - else: - self.network_address = IPv4Address(packed & - int(self.netmask)) - return + super().__init__(address) - # Assume input argument to be string or any object representation - # which converts into a formatted IP prefix string. - addr = _split_optional_netmask(address) - self.network_address = IPv4Address(self._ip_int_from_string(addr[0])) + addr_prefix = self._get_addr_prefix_tuple(address) + self.network_address, (self.netmask, self._prefixlen) = addr_prefix - if len(addr) == 2: - arg = addr[1] - else: - arg = self._max_prefixlen - self.netmask, self._prefixlen = self._make_netmask(arg) - - if strict: - if (IPv4Address(int(self.network_address) & int(self.netmask)) != - self.network_address): + packed = int(self.network_address) + if packed & int(self.netmask) != packed: + if strict: raise ValueError('%s has host bits set' % self) - self.network_address = IPv4Address(int(self.network_address) & - int(self.netmask)) + else: + self.network_address = IPv4Address(packed & + int(self.netmask)) if self._prefixlen == (self._max_prefixlen - 1): self.hosts = self.__iter__ @@ -1616,14 +1542,26 @@ class _BaseV6: Argument can be: - an integer (the prefix length) - a string representing the prefix length (e.g. "24") - - a string representing the prefix netmask (e.g. "255.255.255.0") + - a bytes object representing the netmask (e.g. b'\xff'*16) + - an IPv6Address object representing the netmask """ if arg not in cls._netmask_cache: if isinstance(arg, int): prefixlen = arg + netmask = IPv6Address(cls._ip_int_from_prefix(prefixlen)) else: - prefixlen = cls._prefix_from_prefix_string(arg) - netmask = IPv6Address(cls._ip_int_from_prefix(prefixlen)) + if isinstance(arg, (bytes, IPv6Address)): + netmask = arg + if isinstance(netmask, bytes): + netmask = IPv6Address(netmask) + try: + prefixlen = cls._prefix_from_ip_int(netmask._ip) + except ValueError: + cls._report_invalid_netmask(arg) + else: + # Check for a netmask in prefix length form + prefixlen = cls._prefix_from_prefix_string(arg) + netmask = IPv6Address(cls._ip_int_from_prefix(prefixlen)) cls._netmask_cache[arg] = netmask, prefixlen return cls._netmask_cache[arg] @@ -1855,6 +1793,24 @@ class _BaseV6: return '%s/%d' % (':'.join(parts), self._prefixlen) return ':'.join(parts) + @classmethod + def _get_addr_prefix_tuple(cls, arg): + """Make a (address, (netmask, prefix)) tuple from the given argument. + + Args: + arg: A string, bytes object or a tuple representing an address and + a network prefix. + + Returns: + A (address, (netmask, prefix)) tuple. + """ + addr, prefix = cls._split_addr_prefix(arg) + if isinstance(addr, IPv6Address): + addr = IPv6Address(addr._ip) + else: + addr = IPv6Address(addr) + return (IPv6Address(addr), cls._make_netmask(prefix)) + def _reverse_pointer(self): """Return the reverse DNS pointer name for the IPv6 address. @@ -2056,27 +2012,12 @@ class IPv6Address(_BaseV6, _BaseAddress): class IPv6Interface(IPv6Address): def __init__(self, address): - if isinstance(address, (bytes, int)): - IPv6Address.__init__(self, address) - self.network = IPv6Network(self._ip) - self._prefixlen = self._max_prefixlen - return - if isinstance(address, tuple): - IPv6Address.__init__(self, address[0]) - if len(address) > 1: - self._prefixlen = int(address[1]) - else: - self._prefixlen = self._max_prefixlen - self.network = IPv6Network(address, strict=False) - self.netmask = self.network.netmask - self.hostmask = self.network.hostmask - return + addr_prefix = self._get_addr_prefix_tuple(address) + addr, (self.netmask, self._prefixlen) = addr_prefix + super().__init__(addr._ip) - addr = _split_optional_netmask(address) - IPv6Address.__init__(self, addr[0]) - self.network = IPv6Network(address, strict=False) + self.network = IPv6Network((addr, self._prefixlen), strict=False) self.netmask = self.network.netmask - self._prefixlen = self.network._prefixlen self.hostmask = self.network.hostmask def __str__(self): @@ -2187,48 +2128,18 @@ class IPv6Network(_BaseV6, _BaseNetwork): supplied. """ - _BaseNetwork.__init__(self, address) - - # Efficient constructor from integer or packed address - if isinstance(address, (bytes, int)): - self.network_address = IPv6Address(address) - self.netmask, self._prefixlen = self._make_netmask(self._max_prefixlen) - return + super().__init__(address) - if isinstance(address, tuple): - if len(address) > 1: - arg = address[1] - else: - arg = self._max_prefixlen - self.netmask, self._prefixlen = self._make_netmask(arg) - self.network_address = IPv6Address(address[0]) - packed = int(self.network_address) - if packed & int(self.netmask) != packed: - if strict: - raise ValueError('%s has host bits set' % self) - else: - self.network_address = IPv6Address(packed & - int(self.netmask)) - return - - # Assume input argument to be string or any object representation - # which converts into a formatted IP prefix string. - addr = _split_optional_netmask(address) + addr_prefix = self._get_addr_prefix_tuple(address) + self.network_address, (self.netmask, self._prefixlen) = addr_prefix - self.network_address = IPv6Address(self._ip_int_from_string(addr[0])) - - if len(addr) == 2: - arg = addr[1] - else: - arg = self._max_prefixlen - self.netmask, self._prefixlen = self._make_netmask(arg) - - if strict: - if (IPv6Address(int(self.network_address) & int(self.netmask)) != - self.network_address): + packed = int(self.network_address) + if packed & int(self.netmask) != packed: + if strict: raise ValueError('%s has host bits set' % self) - self.network_address = IPv6Address(int(self.network_address) & - int(self.netmask)) + else: + self.network_address = IPv6Address(packed & + int(self.netmask)) if self._prefixlen == (self._max_prefixlen - 1): self.hosts = self.__iter__ diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py index 2e31f42..466b42a 100644 --- a/Lib/test/test_ipaddress.py +++ b/Lib/test/test_ipaddress.py @@ -443,6 +443,22 @@ class NetmaskTestMixin_v4(CommonTestMixin_v4): self.assertEqual( str(self.factory('0.0.0.0/%s' % net.hostmask)), net_str) + def test_tuple_constructor(self): + tests = [ + ('192.168.0.0/24', (('192.168.0.0', b'\xff\xff\xff\x00'))), + ('192.168.0.0/23', ((bytes([192, 168, 0, 0]), b'\xff\xff\xfe\x00'))), + ('10.0.0.0/8', + ((ipaddress.IPv4Address('10.0.0.0'), b'\x00\xff\xff\xff'))), + ('10.0.0.0/8', + ((ipaddress.IPv4Interface('10.0.0.0/32'), b'\x00\xff\xff\xff'))), + ('172.16.0.0/16', + (('172.16.0.0', ipaddress.IPv4Address('255.255.0.0')))), + ('172.16.0.0/16', + (('172.16.0.0', ipaddress.IPv4Address('0.0.255.255')))), + ] + for expected, arg in tests: + self.assertInstancesEqual(arg, expected) + def test_netmask_errors(self): def assertBadNetmask(addr, netmask): msg = "%r is not a valid netmask" % netmask @@ -509,6 +525,18 @@ class NetmaskTestMixin_v6(CommonTestMixin_v6): # Zero prefix is treated as decimal. self.assertEqual(str(self.factory('::/0%d' % i)), net_str) + def test_tuple_constructor(self): + tests = [ + ('fc00::/8', (('fc00::', b'\xff' + 15 * b'\x00'))), + ('fc12:3456::/39', + ((b'\xfc\x12\x34\x56' + 12 * b'\x00', + 4 * b'\xff' + b'\xfe' + 11 * b'\x00'))), + ('2001:db8::/32', + (('2001:db8::', ipaddress.IPv6Address('ffff:ffff::')))), + ] + for expected, arg in tests: + self.assertInstancesEqual(arg, expected) + def test_netmask_errors(self): def assertBadNetmask(addr, netmask): msg = "%r is not a valid netmask" % netmask @@ -958,27 +986,12 @@ class IpaddrUnitTest(unittest.TestCase): ipv4_zero_netmask = ipaddress.IPv4Interface('1.2.3.4/0') self.assertEqual(int(ipv4_zero_netmask.network.netmask), 0) self.assertEqual(ipv4_zero_netmask._prefix_from_prefix_string('0'), 0) - self.assertTrue(ipv4_zero_netmask._is_valid_netmask('0')) - self.assertTrue(ipv4_zero_netmask._is_valid_netmask('0.0.0.0')) - self.assertFalse(ipv4_zero_netmask._is_valid_netmask('invalid')) ipv6_zero_netmask = ipaddress.IPv6Interface('::1/0') self.assertEqual(int(ipv6_zero_netmask.network.netmask), 0) self.assertEqual(ipv6_zero_netmask._prefix_from_prefix_string('0'), 0) def testIPv4NetAndHostmasks(self): - net = self.ipv4_network - self.assertFalse(net._is_valid_netmask('invalid')) - self.assertTrue(net._is_valid_netmask('128.128.128.128')) - self.assertFalse(net._is_valid_netmask('128.128.128.127')) - self.assertFalse(net._is_valid_netmask('128.128.128.255')) - self.assertTrue(net._is_valid_netmask('255.128.128.128')) - - self.assertFalse(net._is_hostmask('invalid')) - self.assertTrue(net._is_hostmask('128.255.255.255')) - self.assertFalse(net._is_hostmask('255.255.255.255')) - self.assertFalse(net._is_hostmask('1.2.3.4')) - net = ipaddress.IPv4Network('127.0.0.0/0.0.0.255') self.assertEqual(net.prefixlen, 24)