From f22bf8e3cf899896cf587099d29290cb43aa9724 Mon Sep 17 00:00:00 2001 From: opavliuk <40970635+opavlyuk@users.noreply.github.com> Date: Mon, 31 Jul 2023 16:33:26 +0200 Subject: [PATCH] gh-87799: Improve the textual representation of IPv4-mapped IPv6 addresses (#29345) Represent IPv4-mapped IPv6 address as x:x:x:x:x:x:d.d.d.d, where the 'x's are the hexadecimal values of the six high-order 16-bit pieces of the address, and the 'd's are the decimal values of the four low-order 8-bit pieces of the address (standard IPv4 representation). --------- Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com> Co-authored-by: Andrew Svetlov --- Lib/ipaddress.py | 34 ++++++++++++++++++- Lib/test/test_ipaddress.py | 11 ++++++ .../2021-10-31-16-06-28.bpo-43633.vflwXv.rst | 1 + 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2021-10-31-16-06-28.bpo-43633.vflwXv.rst diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py index af1d5c4800c..f5aba434fd4 100644 --- a/Lib/ipaddress.py +++ b/Lib/ipaddress.py @@ -1923,8 +1923,40 @@ def __init__(self, address): self._ip = self._ip_int_from_string(addr_str) + def _explode_shorthand_ip_string(self): + ipv4_mapped = self.ipv4_mapped + if ipv4_mapped is None: + long_form = super()._explode_shorthand_ip_string() + else: + prefix_len = 30 + raw_exploded_str = super()._explode_shorthand_ip_string() + long_form = "%s%s" % (raw_exploded_str[:prefix_len], str(ipv4_mapped)) + return long_form + + def _ipv4_mapped_ipv6_to_str(self): + """Return convenient text representation of IPv4-mapped IPv6 address + + See RFC 4291 2.5.5.2, 2.2 p.3 for details. + + Returns: + A string, 'x:x:x:x:x:x:d.d.d.d', where the 'x's are the hexadecimal values of + the six high-order 16-bit pieces of the address, and the 'd's are + the decimal values of the four low-order 8-bit pieces of the + address (standard IPv4 representation) as defined in RFC 4291 2.2 p.3. + + """ + ipv4_mapped = self.ipv4_mapped + if ipv4_mapped is None: + raise AddressValueError("Can not apply to non-IPv4-mapped IPv6 address %s" % str(self)) + high_order_bits = self._ip >> 32 + return "%s:%s" % (self._string_from_ip_int(high_order_bits), str(ipv4_mapped)) + def __str__(self): - ip_str = super().__str__() + ipv4_mapped = self.ipv4_mapped + if ipv4_mapped is None: + ip_str = super().__str__() + else: + ip_str = self._ipv4_mapped_ipv6_to_str() return ip_str + '%' + self._scope_id if self._scope_id else ip_str def __hash__(self): diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py index a5388b2e5de..6f204948c9f 100644 --- a/Lib/test/test_ipaddress.py +++ b/Lib/test/test_ipaddress.py @@ -1321,6 +1321,17 @@ def testGetIp(self): self.assertEqual(str(self.ipv6_scoped_interface.ip), '2001:658:22a:cafe:200::1') + def testIPv6IPv4MappedStringRepresentation(self): + long_prefix = '0000:0000:0000:0000:0000:ffff:' + short_prefix = '::ffff:' + ipv4 = '1.2.3.4' + ipv6_ipv4_str = short_prefix + ipv4 + ipv6_ipv4_addr = ipaddress.IPv6Address(ipv6_ipv4_str) + ipv6_ipv4_iface = ipaddress.IPv6Interface(ipv6_ipv4_str) + self.assertEqual(str(ipv6_ipv4_addr), ipv6_ipv4_str) + self.assertEqual(ipv6_ipv4_addr.exploded, long_prefix + ipv4) + self.assertEqual(str(ipv6_ipv4_iface.ip), ipv6_ipv4_str) + def testGetScopeId(self): self.assertEqual(self.ipv6_address.scope_id, None) diff --git a/Misc/NEWS.d/next/Library/2021-10-31-16-06-28.bpo-43633.vflwXv.rst b/Misc/NEWS.d/next/Library/2021-10-31-16-06-28.bpo-43633.vflwXv.rst new file mode 100644 index 00000000000..025de1e1a7d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-10-31-16-06-28.bpo-43633.vflwXv.rst @@ -0,0 +1 @@ +Improve the textual representation of IPv4-mapped IPv6 addresses (:rfc:`4291` Sections 2.2, 2.5.5.2) in :mod:`ipaddress`. Patch by Oleksandr Pavliuk.