diff --git a/CHANGELOG.md b/CHANGELOG.md index f58d8553c9..9e26c6bba6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -350,6 +350,7 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r - message text: the `epb_flags` and `pack_flags` length errors say `expected 4`, the length they check, rather than `expected 8` ([#2168](https://github.com/JarryShaw/PyPCAPKit/issues/2168), [#2179](https://github.com/JarryShaw/PyPCAPKit/pull/2179)), and the IPv4 TS option's unknown-flag warning and the Mobility Header's prefix error spell `timestamp` and `mobile` ([#2171](https://github.com/JarryShaw/PyPCAPKit/issues/2171), [#2189](https://github.com/JarryShaw/PyPCAPKit/pull/2189)). - IPX dissects its payload from a registry of its own, `IPX.__proto__`, keyed by IPX Packet Type, where it looked the Packet Type up as an IP protocol number, so a type 17 (NCP) payload parsed as `IPX:UDP` and type 4 as `IPX:IPv4`. No IPX-native protocol ships, so the payload is `Raw` unless one is registered ([#2169](https://github.com/JarryShaw/PyPCAPKit/issues/2169), [#2187](https://github.com/JarryShaw/PyPCAPKit/pull/2187)). - HIP's `ECHO_RESPONSE_UNSIGNED` maker builds an `EchoResponseUnsignedParameter` schema, where it built the request's; the octets were already right ([#2170](https://github.com/JarryShaw/PyPCAPKit/issues/2170), [#2188](https://github.com/JarryShaw/PyPCAPKit/pull/2188)). +- The MPL option's `seed_type` and the SMF_DPD option's `tid_type` makers (of both HOPOPT and IPv6-Opts), and the HTTP/1.* start line's `status`, resolve a `str` enum name against their own enum when no `..._namespace` is given, where the `None` default made a name raise `ProtocolNotImplemented` ([#2356](https://github.com/JarryShaw/PyPCAPKit/issues/2356)). ### pcapkit.toolkit diff --git a/docs/source/changelog/1.5.0.rst b/docs/source/changelog/1.5.0.rst index f083f7c020..d78de75bc4 100644 --- a/docs/source/changelog/1.5.0.rst +++ b/docs/source/changelog/1.5.0.rst @@ -2755,6 +2755,11 @@ Fixed * HIP's ``ECHO_RESPONSE_UNSIGNED`` maker builds an ``EchoResponseUnsignedParameter`` schema, where it built the request's; the octets were already right (:issue:`2170`, :pr:`2188`). +* The MPL option's ``seed_type`` and the SMF_DPD option's ``tid_type`` makers (of + both HOPOPT and IPv6-Opts), and the HTTP/1.* start line's ``status``, resolve a + ``str`` enum name against their own enum when no ``..._namespace`` is given, + where the ``None`` default made a name raise :exc:`.ProtocolNotImplemented` + (:issue:`2356`). pcapkit.toolkit --------------- diff --git a/pcapkit/protocols/application/httpv1.py b/pcapkit/protocols/application/httpv1.py index fdb673b2ae..512be8f2dd 100644 --- a/pcapkit/protocols/application/httpv1.py +++ b/pcapkit/protocols/application/httpv1.py @@ -348,7 +348,10 @@ def _make_start_line(self, http_version: 'str | bytes', *, header_line = b'%s %s HTTP/%s\r\n' % (meth, uri_val, version) elif method is None and status is not None: - status_code = self._make_index(status, status_default, namespace=status_namespace, + # NOTE: ``status`` defaults to ``None``, so a :obj:`str` name has to be + # resolved against StatusCode when no namespace is given (#2356). + status_ns = status_namespace if status_namespace is not None else Enum_StatusCode + status_code = self._make_index(status, status_default, namespace=status_ns, reversed=status_reversed, pack=False) status_code_val = int(status_code) diff --git a/pcapkit/protocols/internet/hopopt.py b/pcapkit/protocols/internet/hopopt.py index 81376f5027..c72d3bfde3 100644 --- a/pcapkit/protocols/internet/hopopt.py +++ b/pcapkit/protocols/internet/hopopt.py @@ -1744,7 +1744,10 @@ def _make_opt_smf_dpd(self, code: 'Enum_Option', opt: 'Optional[Data_SMFIdentifi tid_len = len(tid_bytes) if tid_type is not None: - tid_kind = self._make_index(tid_type, tid_type_default, namespace=tid_type_namespace, + # NOTE: ``tid_type`` defaults to ``None``, so a :obj:`str` name has to be + # resolved against TaggerID when no namespace is given (#2356). + tid_type_ns = tid_type_namespace if tid_type_namespace is not None else Enum_TaggerID + tid_kind = self._make_index(tid_type, tid_type_default, namespace=tid_type_ns, reversed=tid_type_reversed, pack=False) elif tid_len == 0: tid_kind = Enum_TaggerID.NULL @@ -2054,7 +2057,10 @@ def _make_opt_mpl(self, code: 'Enum_Option', opt: 'Optional[Data_MPLOption]' = N seed = opt.seed_id if seed_type is not None: - kind = self._make_index(seed_type, seed_type_default, namespace=seed_type_namespace, + # NOTE: ``seed_type`` defaults to ``None``, so a :obj:`str` name has to be + # resolved against SeedID when no namespace is given (#2356). + seed_type_ns = seed_type_namespace if seed_type_namespace is not None else Enum_SeedID + kind = self._make_index(seed_type, seed_type_default, namespace=seed_type_ns, reversed=seed_type_reversed, pack=False) elif seed is None or isinstance(seed, ipaddress.IPv6Address): kind = Enum_SeedID.IPV6_SOURCE_ADDRESS diff --git a/pcapkit/protocols/internet/ipv6_opts.py b/pcapkit/protocols/internet/ipv6_opts.py index fe62f868d4..692f17d72f 100644 --- a/pcapkit/protocols/internet/ipv6_opts.py +++ b/pcapkit/protocols/internet/ipv6_opts.py @@ -1732,7 +1732,10 @@ def _make_opt_smf_dpd(self, code: 'Enum_Option', opt: 'Optional[Data_SMFIdentifi tid_len = len(tid_bytes) if tid_type is not None: - tid_kind = self._make_index(tid_type, tid_type_default, namespace=tid_type_namespace, + # NOTE: ``tid_type`` defaults to ``None``, so a :obj:`str` name has to be + # resolved against TaggerID when no namespace is given (#2356). + tid_type_ns = tid_type_namespace if tid_type_namespace is not None else Enum_TaggerID + tid_kind = self._make_index(tid_type, tid_type_default, namespace=tid_type_ns, reversed=tid_type_reversed, pack=False) elif tid_len == 0: tid_kind = Enum_TaggerID.NULL @@ -2042,7 +2045,10 @@ def _make_opt_mpl(self, code: 'Enum_Option', opt: 'Optional[Data_MPLOption]' = N seed = opt.seed_id if seed_type is not None: - kind = self._make_index(seed_type, seed_type_default, namespace=seed_type_namespace, + # NOTE: ``seed_type`` defaults to ``None``, so a :obj:`str` name has to be + # resolved against SeedID when no namespace is given (#2356). + seed_type_ns = seed_type_namespace if seed_type_namespace is not None else Enum_SeedID + kind = self._make_index(seed_type, seed_type_default, namespace=seed_type_ns, reversed=seed_type_reversed, pack=False) elif seed is None or isinstance(seed, ipaddress.IPv6Address): kind = Enum_SeedID.IPV6_SOURCE_ADDRESS diff --git a/tests/protocols/test_make_str_enum_name_none_default_2356_unit.py b/tests/protocols/test_make_str_enum_name_none_default_2356_unit.py new file mode 100644 index 0000000000..1d1723fe3c --- /dev/null +++ b/tests/protocols/test_make_str_enum_name_none_default_2356_unit.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +"""GitHub issue #2356: makers resolve a :obj:`str` enum name for a ``None`` default. + +:meth:`~pcapkit.protocols.protocol.Protocol._make_index` resolves a :obj:`str` +name against the enum of the argument's default when no ``..._namespace`` is +given, so the makers whose argument defaults to ``None`` -- the MPL option's +``seed_type`` and the SMF_DPD option's ``tid_type`` (of both HOPOPT and +IPv6-Opts), and the HTTP/1.* start line's ``status`` -- raised +:exc:`~pcapkit.utilities.exceptions.ProtocolNotImplemented` for a name such as +``'IPv4'``. They now fall back to the argument's own enum, and a name builds +the same octets as the member it names. + +The classes are imported inside each test, after +:func:`~tests._support.reimport_once_per_class`, so that they belong to the +:mod:`pcapkit` import that is live when the test runs. + +""" + +import importlib +import unittest + +from tests._support import reimport_once_per_class + +#: The protocol classes sharing the option makers, by module and class name. +PROTOCOLS = ( + ('pcapkit.protocols.internet.hopopt', 'HOPOPT'), + ('pcapkit.protocols.internet.ipv6_opts', 'IPv6_Opts'), +) + + +class TestMakeStrEnumNameNoneDefault(unittest.TestCase): + """Pin :obj:`str` enum names for the makers whose argument defaults to ``None``.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def _protocols(self) -> 'list[type]': + return [getattr(importlib.import_module(mod), name) for mod, name in PROTOCOLS] + + def test_mpl_seed_type_by_name(self) -> None: + from pcapkit.const.ipv6.option import Option + from pcapkit.const.ipv6.seed_id import SeedID + + for cls in self._protocols(): + proto = object.__new__(cls) + for member in (SeedID.SEEDID_16_BIT_UNSIGNED_INTEGER, SeedID.SEEDID_64_BIT_UNSIGNED_INTEGER): + with self.subTest(protocol=cls.__name__, seed_type=member.name): + by_member = proto._make_opt_mpl(Option.MPL_Option, seed_type=member, seed=1) + by_name = proto._make_opt_mpl(Option.MPL_Option, seed_type=member.name, seed=1) + self.assertEqual(by_name.pack(), by_member.pack()) + self.assertEqual(by_name.flags['type'], int(member)) + + def test_smf_dpd_tid_type_by_name(self) -> None: + from pcapkit.const.ipv6.option import Option + from pcapkit.const.ipv6.tagger_id import TaggerID + + for cls in self._protocols(): + proto = object.__new__(cls) + for member, tid in ((TaggerID.IPv4, b'\x0a\x00\x00\x01'), (TaggerID.DEFAULT, b'\x01\x02')): + with self.subTest(protocol=cls.__name__, tid_type=member.name): + by_member = proto._make_opt_smf_dpd(Option.SMF_DPD, mode='I_DPD', tid_type=member, + tid=tid, id=b'\x01') + by_name = proto._make_opt_smf_dpd(Option.SMF_DPD, mode='I_DPD', tid_type=member.name, + tid=tid, id=b'\x01') + self.assertEqual(by_name.pack(), by_member.pack()) + + def test_http_status_by_name(self) -> None: + from pcapkit.const.http.status_code import StatusCode + from pcapkit.protocols.application.httpv1 import HTTP + + proto = object.__new__(HTTP) + for member in (StatusCode.CODE_200, StatusCode.CODE_404): + with self.subTest(status=member.name): + line = proto._make_start_line('1.1', status=member.name) + self.assertTrue(line.startswith(b'HTTP/1.1 %d ' % int(member)), line) + + def test_an_explicit_namespace_still_wins(self) -> None: + from pcapkit.const.ipv6.option import Option + from pcapkit.const.ipv6.seed_id import SeedID + + for cls in self._protocols(): + proto = object.__new__(cls) + with self.subTest(protocol=cls.__name__): + code = int(SeedID.SEEDID_16_BIT_UNSIGNED_INTEGER) + made = proto._make_opt_mpl(Option.MPL_Option, seed_type='short', seed=1, + seed_type_namespace={code: 'short'}) + self.assertEqual(made.flags['type'], code) + + +if __name__ == '__main__': + unittest.main()