From 6ad13e10138433bb103a310863cf7a6c4f4f55d7 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Wed, 7 Oct 2026 00:46:12 -0400 Subject: [PATCH] fix(hip): from_data no longer reads alias before _info is set (#1184) Outside an IPv6 extension chain, HIP.read ends in _decode_next_layer, which builds the protocol chain from self.alias. alias reads self._info.version, but __post_init__ assigns _info only after read returns, so parsing or constructing a standalone HIP header, and HIP.from_data over a parsed one, raised AttributeError. read now sets _info just before decoding the next layer. All 49 HIP headers in options-internet.pcap now rebuild byte for byte through HIP.from_data (49 raised before). --- pcapkit/protocols/internet/hip.py | 5 ++ .../internet/test_hip_from_data_alias_unit.py | 87 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 tests/protocols/internet/test_hip_from_data_alias_unit.py diff --git a/pcapkit/protocols/internet/hip.py b/pcapkit/protocols/internet/hip.py index 89a5d1bd0a..4aa9babba5 100644 --- a/pcapkit/protocols/internet/hip.py +++ b/pcapkit/protocols/internet/hip.py @@ -592,6 +592,11 @@ def read(self, length: 'Optional[int]' = None, *, extension: bool = False, **kwa if extension: return hip + + # NOTE: ``_decode_next_layer`` builds the protocol chain from + # :attr:`self.alias `, which reads ``self._info``, and + # ``__post_init__`` assigns that only once this method returns. + self._info = hip return self._decode_next_layer(hip, schema.next, length - hip.length) def make(self, diff --git a/tests/protocols/internet/test_hip_from_data_alias_unit.py b/tests/protocols/internet/test_hip_from_data_alias_unit.py new file mode 100644 index 0000000000..2705114f2e --- /dev/null +++ b/tests/protocols/internet/test_hip_from_data_alias_unit.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +"""HIP's ``alias`` is readable while the header is still being decoded. + +GitHub issue #1184: :attr:`HIP.alias ` +read ``self._info.version``. Outside an IPv6 extension chain, +:meth:`HIP.read ` ends in +``_decode_next_layer``, which reads ``alias`` for the protocol chain, but +``_info`` is assigned only once ``read`` has returned. So parsing or +constructing a standalone HIP header, and +:meth:`~pcapkit.protocols.protocol.ProtocolBase.from_data` over a parsed one, +raised ``AttributeError: 'HIP' object has no attribute '_info'``. + +Every case builds its own octets in memory and reads no capture. :class:`HIP` +is imported inside each test, after +:func:`~tests._support.reimport_once_per_class`, not at module load. + +""" + +import importlib.util +import unittest + +from tests._support import reimport_once_per_class + +RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') +HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) + +#: Header fields shared by every HIP packet built here, as in +#: :data:`examples.generators.options.HIP_BASE`. +HIP_BASE = { + 'next': 59, 'packet': 1, 'checksum': b'\x00\x00', + 'controls_anonymous': False, 'shit': 0, 'rhit': 0, 'payload': b'', +} + + +@unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') +class TestHIPFromDataAlias(unittest.TestCase): + """Pin ``alias`` on the paths that decode a next layer.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + @staticmethod + def _parameters() -> 'list': + from pcapkit.const.hip.parameter import Parameter + + return [(Parameter.R1_COUNTER, {'counter': 0x0123456789abcdef})] + + def _octets(self, version: 'int') -> 'bytes': + from pcapkit.protocols.internet.hip import HIP + + return HIP(parameters=self._parameters(), extension=True, + version=version, **HIP_BASE).data + + def test_construct_standalone_header(self) -> None: + from pcapkit.protocols.internet.hip import HIP + + for version in (1, 2): + with self.subTest(version=version): + proto = HIP(parameters=self._parameters(), version=version, **HIP_BASE) + self.assertEqual(proto.alias, f'HIPv{version}') + self.assertEqual(proto.data, self._octets(version)) + + def test_parse_standalone_header(self) -> None: + from pcapkit.protocols.internet.hip import HIP + + for version in (1, 2): + with self.subTest(version=version): + octets = self._octets(version) + proto = HIP(octets, len(octets)) + self.assertEqual(proto.alias, f'HIPv{version}') + self.assertEqual(str(proto.protochain).split(':')[0], f'HIPv{version}') + + def test_from_data_rebuilds_extension_header_byte_for_byte(self) -> None: + from pcapkit.protocols.internet.hip import HIP + + for version in (1, 2): + with self.subTest(version=version): + octets = self._octets(version) + parsed = HIP(octets, len(octets), extension=True) + rebuilt = HIP.from_data(parsed.info) + self.assertEqual(rebuilt.data, octets) + self.assertEqual(rebuilt.alias, f'HIPv{version}') + self.assertEqual(rebuilt.info.version, version) + + +if __name__ == '__main__': + unittest.main()