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: