diff --git a/docs/source/pcapkit/const/linux.rst b/docs/source/pcapkit/const/linux.rst index b332b35c9d..30cded4b64 100644 --- a/docs/source/pcapkit/const/linux.rst +++ b/docs/source/pcapkit/const/linux.rst @@ -25,6 +25,7 @@ which is automatically generated from :class:`pcapkit.vendor.linux.arphrd.ARPHRD :members: :undoc-members: :show-inheritance: + :special-members: __proto_dest__ .. rubric:: Footnotes diff --git a/pcapkit/const/linux/arphrd.py b/pcapkit/const/linux/arphrd.py index 0bad7cc306..a7656a1e90 100644 --- a/pcapkit/const/linux/arphrd.py +++ b/pcapkit/const/linux/arphrd.py @@ -15,6 +15,7 @@ from aenum import IntEnum from pcapkit.corekit.enum import EnumRegistry +from pcapkit.corekit.module import ModuleDescriptor from pcapkit.utilities.exceptions import EnumValueError if TYPE_CHECKING: @@ -26,6 +27,17 @@ class ARPHRD(EnumRegistry, IntEnum): """[ARPHRD] Linux ARP hardware types [````]""" + #: Protocol class(es) owning the ``__proto__`` dispatch registry keyed by + #: this enumeration, read by + #: :func:`~pcapkit.foundation.registry.protocols.register_protocol_code` + #: to resolve a bare member of it into its destination(s). The classes are + #: named as lazy :class:`~pcapkit.corekit.module.ModuleDescriptor` + #: references rather than imported, because :mod:`pcapkit.const` sits below + #: :mod:`pcapkit.protocols` and importing one from here would be circular. + __proto_dest__ = ( + ModuleDescriptor('pcapkit.protocols.link.linux_sll', 'LinuxSLL'), + ) + #: NET/ROM pseudo. NETROM = 0 diff --git a/pcapkit/foundation/registry/protocols.py b/pcapkit/foundation/registry/protocols.py index 811ccbc181..fc84b3f080 100644 --- a/pcapkit/foundation/registry/protocols.py +++ b/pcapkit/foundation/registry/protocols.py @@ -517,7 +517,9 @@ def register_protocol_code(protocol: 'Type[ProtocolBase]', code: 'Any') -> 'None :class:`~pcapkit.const.reg.transtype.TransType` to :class:`~pcapkit.protocols.internet.internet.Internet`, :class:`~pcapkit.const.sctp.payload_protocol_identifier.PayloadProtocolIdentifier` - to :class:`~pcapkit.protocols.transport.sctp.SCTP`, and + to :class:`~pcapkit.protocols.transport.sctp.SCTP`, + :class:`~pcapkit.const.linux.arphrd.ARPHRD` to + :class:`~pcapkit.protocols.link.linux_sll.LinuxSLL`, and :class:`~pcapkit.const.reg.linktype.LinkType` to *both* :class:`~pcapkit.protocols.misc.pcap.frame.Frame` **and** :class:`~pcapkit.protocols.misc.pcapng.PCAPNG`; or diff --git a/pcapkit/vendor/linux/arphrd.py b/pcapkit/vendor/linux/arphrd.py index 87a8052ee1..8c59f6d854 100644 --- a/pcapkit/vendor/linux/arphrd.py +++ b/pcapkit/vendor/linux/arphrd.py @@ -13,6 +13,7 @@ import sys from typing import TYPE_CHECKING +from pcapkit.corekit.module import ModuleDescriptor from pcapkit.vendor.default import Vendor if TYPE_CHECKING: @@ -59,6 +60,10 @@ class ARPHRD(Vendor): #: Value limit checker. FLAG = 'isinstance(value, int) and 0x0000 <= value <= 0xFFFF' + #: Destination of a bare ``ARPHRD`` member; see + #: :attr:`~pcapkit.vendor.default.Vendor.PROTO_DEST`. + PROTO_DEST = (ModuleDescriptor('pcapkit.protocols.link.linux_sll', 'LinuxSLL'),) + def request(self) -> 'dict[int, tuple[str, str]]': # type: ignore[override] # pylint: disable=arguments-differ """Fetch registry data. diff --git a/tests/protocols/test_dispatch_reachability_unit.py b/tests/protocols/test_dispatch_reachability_unit.py index 48d4b879a1..d18792effe 100644 --- a/tests/protocols/test_dispatch_reachability_unit.py +++ b/tests/protocols/test_dispatch_reachability_unit.py @@ -162,10 +162,11 @@ def test_every_indexed_class_is_reachable_under_the_code_it_declares(self) -> No f'{gap.found!r}' for gap in gaps)) # A walk that found nothing to check would also report no gaps, so pin - # a floor of 23 claims (measured: the 21 distinct classes plus ``InARP`` - # and ``DRARP`` reaching their family bases' entries, and ``Ethernet`` - # counted once for ``Frame`` and once for ``PCAPNG``). - self.assertGreaterEqual(checked, 23) + # the measured claim count as a floor: each indexed class once per + # destination its code's ``__proto_dest__`` names -- ``Frame`` and + # ``PCAPNG`` for a ``LinkType`` code, ``LinuxSLL`` for an ``ARPHRD`` one + # -- with ``InARP`` and ``DRARP`` reaching their family bases' entries. + self.assertGreaterEqual(checked, 39) def test_the_audit_detects_a_code_that_nothing_dispatches(self) -> None: """The guard has teeth: inject a gap and confirm the audit reports it. diff --git a/tests/protocols/test_protocol_code_registration_unit.py b/tests/protocols/test_protocol_code_registration_unit.py index 1ddd8e733c..1751556feb 100644 --- a/tests/protocols/test_protocol_code_registration_unit.py +++ b/tests/protocols/test_protocol_code_registration_unit.py @@ -215,6 +215,19 @@ class FakeProtocol: reg_frame.assert_called_once_with(LinkType.RAW, FakeProtocol) reg_pcapng.assert_called_once_with(LinkType.RAW, FakeProtocol) + def test_bare_arphrd_member_infers_linux_sll(self) -> None: + """``ARPHRD`` names ``LinuxSLL``, the family base owning the registry (#2533).""" + from pcapkit.const.linux.arphrd import ARPHRD + from pcapkit.foundation.registry.protocols import register_protocol_code + from pcapkit.protocols.link.linux_sll import LinuxSLL + + class FakeProtocol: + pass + + with mock.patch.object(LinuxSLL, 'register') as register: + register_protocol_code(FakeProtocol, ARPHRD.NETROM) + register.assert_called_once_with(ARPHRD.NETROM, FakeProtocol) + def test_raw_int_without_explicit_destination_refuses(self) -> None: """A bare ``int`` cannot say whether it means TCP or UDP -- it must refuse. @@ -359,6 +372,26 @@ class MyGre(Internet, code=TransType.GRE): self.assertIs(Internet.__proto__[TransType.GRE], MyGre) self.assertNotIn(TransType.GRE, Internet.__proto__) + def test_class_keyword_with_an_arphrd_member_reaches_linux_sll_dispatch(self) -> None: + """``code=ARPHRD.X`` registers where a cooked frame dispatches (#2533).""" + import struct + + from pcapkit.const.linux.arphrd import ARPHRD + from pcapkit.protocols.link.linux_sll import LinuxSLL + from pcapkit.protocols.link.linux_sll2 import LinuxSLL2 + from pcapkit.protocols.misc.raw import Raw + + self.assertNotIn(ARPHRD.NETROM, LinuxSLL.__proto__) + frame = struct.pack('>HHIHBB8s', 0x0800, 0, 0, ARPHRD.NETROM, 0, 0, b'') + b'\xaa\xbb' + + with mock.patch.dict(LinuxSLL.__proto__, {}, clear=False): + class NetROM(Raw, code=ARPHRD.NETROM): + pass + + self.assertIs(LinuxSLL.__proto__[ARPHRD.NETROM], NetROM) + self.assertEqual(LinuxSLL2(frame, len(frame)).protochain.chain, 'LINUX_SLL2:NetROM') + self.assertNotIn(ARPHRD.NETROM, LinuxSLL.__proto__) + def test_subclass_without_code_does_not_inherit_parent_registration(self) -> None: """Opting in is per-subclass, not inherited from a registered parent. @@ -724,6 +757,8 @@ def setUp(self) -> None: ('pcapkit.const.reg.linktype', 'LinkType', (('pcapkit.protocols.misc.pcap.frame', 'Frame'), ('pcapkit.protocols.misc.pcapng', 'PCAPNG'))), + ('pcapkit.const.linux.arphrd', 'ARPHRD', + (('pcapkit.protocols.link.linux_sll', 'LinuxSLL'),)), ) def test_each_registry_declares_the_destinations_it_keys(self) -> None: diff --git a/tests/vendor/test_vendor_proto_dest_2392_unit.py b/tests/vendor/test_vendor_proto_dest_2392_unit.py index 5332593547..aab748dc18 100644 --- a/tests/vendor/test_vendor_proto_dest_2392_unit.py +++ b/tests/vendor/test_vendor_proto_dest_2392_unit.py @@ -23,12 +23,13 @@ touched the other generated modules would rewrite most of :mod:`pcapkit.const` on the next crawl. * :class:`CrawlerProtoDestTests` -- the declaring crawlers name exactly the - destinations the registries need, and no other crawler declares one. + destinations the registries need, no other crawler declares one, and the + committed ``ARPHRD`` module is its crawler's rendering. -Nothing here crawls: :meth:`Vendor.__init__` fetches from IANA and writes the -constant file as a side effect of construction, so the one instance built below -uses ``cls.__new__(cls)``, the same bypass -:file:`tests/vendor/test_vendor_dest_path_unit.py` uses for the same reason. +Nothing here crawls: :meth:`Vendor.__init__` fetches its registry and writes the +constant file as a side effect of construction, so the one crawler constructed +below is ``ARPHRD``, whose registry is a ``DATA`` literal, writing to a +temporary file. """ from __future__ import annotations @@ -53,8 +54,8 @@ HAS_VENDOR_DEPS = all(importlib.util.find_spec(name) is not None for name in VENDOR_DEPS) #: ``crawler module -> ((module, class), ...)``, the destinations the declaring -#: crawlers name (#2392), with ``LinkType`` naming two: a link-layer header -#: type keys the next-layer registry of both capture file formats. +#: crawlers name (#2392, #2533), with ``LinkType`` naming two: a link-layer +#: header type keys the next-layer registry of both capture file formats. EXPECTED = { 'pcapkit.vendor.reg.ethertype': (('pcapkit.protocols.link.link', 'Link'),), 'pcapkit.vendor.reg.transtype': (('pcapkit.protocols.internet.internet', 'Internet'),), @@ -62,6 +63,7 @@ ('pcapkit.protocols.misc.pcapng', 'PCAPNG')), 'pcapkit.vendor.sctp.payload_protocol_identifier': (('pcapkit.protocols.transport.sctp', 'SCTP'),), + 'pcapkit.vendor.linux.arphrd': (('pcapkit.protocols.link.linux_sll', 'LinuxSLL'),), } @@ -253,6 +255,34 @@ def test_every_declared_destination_resolves_to_a_protocol_class(self) -> None: with self.subTest(crawler=name, destination=descriptor.name): self.assertTrue(issubclass(descriptor.klass, ProtocolBase)) + def test_the_committed_arphrd_module_is_the_crawlers_rendering(self) -> None: + """Regenerating ``ARPHRD`` reproduces the committed module, declaration included (#2533). + + A regeneration pin, not the change's fail-before proof: it holds whenever + crawler and committed module agree, so it passes without the declaration + too. ``EXPECTED`` and the inference tests in + :file:`tests/protocols/test_protocol_code_registration_unit.py` are what + fail without it. + + Its crawler reads a ``DATA`` literal rather than fetching, so the whole + render runs offline; it writes to a temporary file here, not to + :file:`pcapkit/const/`. + """ + import pathlib + import tempfile + from unittest import mock + + import pcapkit.const.linux.arphrd as committed + from pcapkit.vendor.linux.arphrd import ARPHRD + + with tempfile.TemporaryDirectory(prefix='pcapkit-vendor-arphrd-') as tmp: + dest = pathlib.Path(tmp, 'arphrd.py') + with mock.patch.object(ARPHRD, '_dest_path', return_value=str(dest)): + ARPHRD() + rendered = dest.read_text(encoding='utf-8') + + self.assertEqual(rendered, pathlib.Path(committed.__file__).read_text(encoding='utf-8')) + if __name__ == '__main__': unittest.main()