diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index e8ecd15..f0fafac 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -25,6 +25,11 @@ This library adheres to `Semantic Versioning 2.0 `_. deciding whether to add them to the string reference namespace, desynchronising it from the decoder (which counts bytes) and corrupting later string references for non-ASCII strings (`#314 `_; PR by @sahvx655-wq) +- Fixed the decoder rejecting scoped IPv6 addresses (tag 54) with a ``CBORDecodeError`` reading + ``invalid types in input array``; the encoder emits them as ``[address, null, zone id]`` but the + decoder only handled the network and interface array forms, so a scoped + :class:`~ipaddress.IPv6Address` could not be decoded back + (`#324 `_; PR by @sahvx655-wq) **6.1.2** (2026-06-02) diff --git a/rust/decoder.rs b/rust/decoder.rs index 0cf7598..0d2b26c 100644 --- a/rust/decoder.rs +++ b/rust/decoder.rs @@ -1208,35 +1208,12 @@ impl CBORDecoder { { // The decoded value was a 2-item (or 3 with zone ID) array. // Check the types of the elements: - // (int, bytes) -> network - // (bytes, int) -> interface + // (bytes, null) -> scoped address + // (int, bytes) -> network + // (bytes, int) -> interface let first_item = tuple.get_item(0)?; let second_item = tuple.get_item(1)?; let zone_id = tuple.get_item(2).ok(); - let (class, addr_bytes, prefix) = if let Ok(prefix) = first_item.cast::() - && let Ok(address) = second_item.cast::() - { - let mut address_vec: Vec = address.extract()?; - if address_vec.len() > 16 { - return Err(CBORDecodeError::new_err(format!( - "address byte string for IPv6 network is too long ({} bytes)", - address_vec.len() - ))); - } - address_vec.resize(16, 0); - Ok(( - IPV6NETWORK_TYPE.get(py)?, - PyBytes::new(py, address_vec.as_slice()), - prefix, - )) - } else if let Ok(address) = first_item.cast_into::() - && let Ok(prefix) = second_item.cast::() - { - Ok((IPV6INTERFACE_TYPE.get(py)?, address, prefix)) - } else { - Err(CBORDecodeError::new_err("invalid types in input array")) - }?; - let addr_obj = ipv6addr_class.call1((addr_bytes,))?; // Format the zone ID suffix if a zone ID was included // (bytes or integer as the last item of a 3-tuple) @@ -1255,8 +1232,41 @@ impl CBORDecoder { String::default() }; - let formatted_addr = format!("{addr_obj}{zone_id_suffix}/{prefix}"); - class.call1((formatted_addr,))? + if second_item.is_none() + && let Ok(address) = first_item.cast::() + { + // A scoped address is encoded as [address, null, zone id]; with no prefix + // length it decodes to a bare IPv6 address rather than a network or interface. + let addr_obj = ipv6addr_class.call1((address,))?; + ipv6addr_class.call1((format!("{addr_obj}{zone_id_suffix}"),))? + } else { + let (class, addr_bytes, prefix) = if let Ok(prefix) = first_item.cast::() + && let Ok(address) = second_item.cast::() + { + let mut address_vec: Vec = address.extract()?; + if address_vec.len() > 16 { + return Err(CBORDecodeError::new_err(format!( + "address byte string for IPv6 network is too long ({} bytes)", + address_vec.len() + ))); + } + address_vec.resize(16, 0); + Ok(( + IPV6NETWORK_TYPE.get(py)?, + PyBytes::new(py, address_vec.as_slice()), + prefix, + )) + } else if let Ok(address) = first_item.cast_into::() + && let Ok(prefix) = second_item.cast::() + { + Ok((IPV6INTERFACE_TYPE.get(py)?, address, prefix)) + } else { + Err(CBORDecodeError::new_err("invalid types in input array")) + }?; + let addr_obj = ipv6addr_class.call1((addr_bytes,))?; + let formatted_addr = format!("{addr_obj}{zone_id_suffix}/{prefix}"); + class.call1((formatted_addr,))? + } } else { return Err(CBORDecodeError::new_err( "input value must be a bytestring or an array of 2 elements", diff --git a/tests/test_decoder.py b/tests/test_decoder.py index c4e3328..bda4e0d 100644 --- a/tests/test_decoder.py +++ b/tests/test_decoder.py @@ -873,6 +873,16 @@ def test_uuid_invalid_type() -> None: IPv6Interface("fe80::202:2ff:ffff:fe03:303%2/64"), id="ipv6if_num_zoneid", ), + pytest.param( + "d8368350fe8000000000020202fffffffe030303f64465746830", + IPv6Address("fe80::202:2ff:ffff:fe03:303%eth0"), + id="ipv6addr_str_zoneid", + ), + pytest.param( + "d8368350fe8000000000020202fffffffe030303f64132", + IPv6Address("fe80::202:2ff:ffff:fe03:303%2"), + id="ipv6addr_num_zoneid", + ), ], ) def test_ipaddress(payload: bytes, expected: Any) -> None: