diff --git a/docs/source/contributing/conventions/index.rst b/docs/source/contributing/conventions/index.rst index 6ee1f7abd6..e5f808118f 100644 --- a/docs/source/contributing/conventions/index.rst +++ b/docs/source/contributing/conventions/index.rst @@ -13,8 +13,9 @@ House Conventions * :ref:`mint-criterion` -- :mod:`pcapkit.const`, which is where the settled questions have mostly arisen; * :ref:`sentinel-convention` -- :mod:`pcapkit.corekit`; - * :ref:`registry-protocol` -- :mod:`pcapkit.const` again, and the registration - wrappers of :mod:`pcapkit.foundation.registry`; + * :ref:`registry-protocol` -- :mod:`pcapkit.const` again, the registration + wrappers of :mod:`pcapkit.foundation.registry`, and the value a protocol's + :meth:`~pcapkit.protocols.protocol.Protocol.__index__` returns; * :ref:`extension-header-subclassing` -- a protocol class hierarchy; * :ref:`protocol-family-grouping` -- how a protocol family's base and members spell :meth:`~pcapkit.protocols.protocol.Protocol.id`, how a protocol class diff --git a/docs/source/contributing/conventions/registry-protocol.rst b/docs/source/contributing/conventions/registry-protocol.rst index 591d25cf6d..20b049ad62 100644 --- a/docs/source/contributing/conventions/registry-protocol.rst +++ b/docs/source/contributing/conventions/registry-protocol.rst @@ -863,3 +863,86 @@ unless its ``EXCUSED`` table gives the reason. It fails as well on a map whose o defines no ``register*`` classmethod of its own, unless its ``INHERITED`` table names the one typed for that map's keys. +.. _registry-protocol-index: + +The Registry Index +~~~~~~~~~~~~~~~~~~ + +**A protocol's** :meth:`~pcapkit.protocols.protocol.Protocol.__index__` **returns its** +:class:`~pcapkit.const.reg.linktype.LinkType`, :class:`~pcapkit.const.reg.ethertype.EtherType`, +:class:`~pcapkit.const.reg.transtype.TransType` **or** +:class:`~pcapkit.const.reg.apptype.apptype.AppType` **member wherever one names it**, ahead of +any other registry's code, such as an :class:`~pcapkit.const.linux.arphrd.ARPHRD` type or an +NLPID. Ruled on :issue:`2627`. So :class:`~pcapkit.protocols.link.radiotap.Radiotap` returns +``LinkType.IEEE802_11_RADIOTAP`` rather than ``ARPHRD.IEEE80211_RADIOTAP``. + +* **An AppType value is returned as its port.** An + :class:`~pcapkit.const.reg.apptype.apptype.AppType` member is a :class:`str`, and + :func:`int` and :func:`operator.index` raise :exc:`TypeError` when ``__index__`` returns + anything but an :class:`int`, so a class an AppType member names returns that member's + port: :class:`~pcapkit.protocols.link.l2tpv2.L2TPv2` returns 1701, ``int(UDP.l2tp)``. + :class:`~pcapkit.protocols.application.tls.TLS`, which + :class:`~pcapkit.protocols.application.sslv3.SSLv3` inherits, and + :class:`~pcapkit.protocols.application.dtls.DTLS` return the ports TCP and UDP dispatch + them at, 443 and 5684, although IANA registers those ports as ``https`` and ``coaps``, + the protocols carried over them. Ruled on :issue:`2627`. +* **Netlink keeps** ``ARPHRD.NETLINK``: ``LINKTYPE_NETLINK`` (253) names the SLL-framed + form, not the ``nlmsghdr`` :class:`~pcapkit.protocols.link.netlink.Netlink` reads. Ruled + on :issue:`2627`. +* **The index keys no registration.** Every built-in ``__proto__`` entry spells out its key, + so a class keeps its place in a secondary registry whatever its index: + :class:`~pcapkit.protocols.link.frad.FRAD` stays under ``ARPHRD.FRAD`` in + :attr:`LinuxSLL.__proto__ `. +* **A class dispatched under several values returns one of them.** + :class:`~pcapkit.protocols.internet.ipv4.IPv4` and + :class:`~pcapkit.protocols.internet.ipv6.IPv6`, dispatched under LinkType 228 and 229, + EtherType 0x0800 and 0x86DD, and TransType 4 and 41, and + :class:`~pcapkit.protocols.internet.ipx.IPX`, under EtherType 0x8137 and TransType 111, + keep the TransType value they returned in 1.5.x, so no shipped value changes. + :class:`~pcapkit.protocols.link.loopback.Loopback`, at LinkType 0 and 108, returns + ``LinkType.NULL``, 0, and :class:`~pcapkit.protocols.application.http.HTTP`, at TCP and + UDP ports 80 and 8080, returns 80. +* **An index is a reachability claim** wherever its enum declares ``__proto_dest__``. + :file:`tests/protocols/test_dispatch_reachability_unit.py` requires the class in the + registry that enum keys, or an entry in its ``UNBOUND`` table giving the reason nothing + dispatches it there, which the binding deletes. A port, being an :class:`int`, makes no + such claim. +* **No value, no index.** :class:`~pcapkit.protocols.link.link.Link`, + :class:`~pcapkit.protocols.internet.internet.Internet` and + :class:`~pcapkit.protocols.transport.transport.Transport`, and the + :class:`~pcapkit.protocols.internet.ip.IP`, :class:`~pcapkit.protocols.internet.ipsec.IPsec` + and :class:`~pcapkit.protocols.internet.icmp.ICMP` family bases, leave the abstract method + unimplemented and return :obj:`None`. These raise: + + - :class:`~pcapkit.protocols.link.vlan.VLAN`, + :class:`~pcapkit.protocols.link.linux_sll.LinuxSLL` and + :class:`~pcapkit.protocols.link.l2tp.L2TP`, abstract family bases nothing dispatches + to, whose members each carry a value of their own; + - :class:`~pcapkit.protocols.internet.ipv6_ext.IPv6_Ext`, although + :attr:`Internet.__proto__ ` + dispatches it at TransType 140, Shim6: one class parses Shim6 and any other + :rfc:`6564`-format header registered to it, so its value differs per instance, and + ``__index__`` is a class method; + - :class:`~pcapkit.protocols.application.application.Application`, and the + :class:`~pcapkit.protocols.application.ssl.SSL` family base and + :class:`~pcapkit.protocols.application.sslv2.SSLv2`, which define no ``__index__`` and + inherit its raise. Nothing dispatches to SSLv2: an SSLv2 record on TCP port 443 parses + as a TLS layer adopting SSLv2's data; + - :class:`~pcapkit.protocols.misc.raw.Raw` and + :class:`~pcapkit.protocols.misc.no_payload.NoPayload`, the undissected octets and the + empty payload, and :class:`~pcapkit.protocols.misc.checksum.ip_pseudo.IP_Pseudo`, the + checksum pseudo-header, which no registry value names. TransType 59, under which + :attr:`Internet.__proto__ ` + dispatches Raw, is IPv6-NoNxt, no next header; + - the PCAP :class:`~pcapkit.protocols.misc.pcap.header.Header` and + :class:`~pcapkit.protocols.misc.pcap.frame.Frame`, and + :class:`~pcapkit.protocols.misc.pcapng.PCAPNG`, the capture file's own records. A PCAP + frame or PCAP-NG packet block instance returns its index number, an ordinal outside + the rule. + + A member sharing its family base's value inherits the base's ``__index__`` rather than + overriding it. + +:file:`tests/protocols/test_protocol_index_registry_2627_unit.py` holds every protocol +class to the rule, with each value derived from the registry and the number it assigns. + diff --git a/docs/source/contributing/pep.rst b/docs/source/contributing/pep.rst index e621ef1633..4706b98401 100644 --- a/docs/source/contributing/pep.rst +++ b/docs/source/contributing/pep.rst @@ -336,9 +336,10 @@ Some wanted only a table entry, because the dissector was already there: * **Done.** :class:`~pcapkit.protocols.application.ospf.OSPF` is bound at :class:`~pcapkit.const.reg.transtype.TransType` 89 (``OSPFIGP``) and - :class:`~pcapkit.protocols.link.l2tpv2.L2TPv2` at UDP port 1701. - ``__index__`` raises on both, which is correct: neither is reached through a - link-layer EtherType. + :class:`~pcapkit.protocols.link.l2tpv2.L2TPv2` at UDP port 1701, and + ``__index__`` returns those values: ``TransType.OSPFIGP``, and 1701 as an + :class:`int`, as an AppType-valued index is the member's port + (:ref:`registry-protocol-index`). * **Done.** :class:`~pcapkit.protocols.application.ftp_data.FTP_DATA` is bound at TCP port 20 (IANA ``ftp-data``). It is a thin :class:`~pcapkit.protocols.misc.raw.Raw` subclass, so this buys the payload a @@ -370,11 +371,10 @@ Follow-ups to the bindings above: reason is structural: it references :rfc:`3931`, i.e. L2TPv3 over IP, and **there is no** ``L2TPv3`` **class for it to point at**. What exists is :class:`~pcapkit.protocols.link.l2tpv2.L2TPv2`, the :rfc:`2661` v2 framing, reached over - UDP 1701. So the binding waits on a v3 dissector, which is also the first member of the - family to carry an :meth:`~pcapkit.protocols.protocol.Protocol.__index__` of its own -- - 115 being that index. :mod:`pcapkit.protocols.link.l2tp` records what v3 needs, and what - ``L2F`` needs alongside it: the version nibble reading ``1`` selects L2F [:rfc:`2341`], - a separate protocol, not an earlier L2TP. + UDP 1701, which is its :meth:`~pcapkit.protocols.protocol.Protocol.__index__`. So the + binding waits on a v3 dissector. :mod:`pcapkit.protocols.link.l2tp` records what v3 + needs, and what ``L2F`` needs alongside it: the version nibble reading ``1`` selects + L2F [:rfc:`2341`], a separate protocol, not an earlier L2TP. * The :class:`~pcapkit.protocols.link.l2tp.L2TP` family lives under :mod:`pcapkit.protocols.link` and so reports ``layer == 'Link'``, although it is carried inside UDP. That is deliberate: a tunnelling protocol is placed by what diff --git a/docs/source/pcapkit/protocols/application/dtls.rst b/docs/source/pcapkit/protocols/application/dtls.rst index 99da22d525..ad6f750e85 100644 --- a/docs/source/pcapkit/protocols/application/dtls.rst +++ b/docs/source/pcapkit/protocols/application/dtls.rst @@ -167,6 +167,7 @@ ones on the wire. .. automethod:: analyze_handshake .. automethod:: __length_hint__ + .. automethod:: __index__ .. automethod:: _make_data diff --git a/docs/source/pcapkit/protocols/application/ftp.rst b/docs/source/pcapkit/protocols/application/ftp.rst index b2b720d55b..0ab9d05089 100644 --- a/docs/source/pcapkit/protocols/application/ftp.rst +++ b/docs/source/pcapkit/protocols/application/ftp.rst @@ -35,6 +35,8 @@ which implements an extractor for File Transfer Protocol .. automethod:: read .. automethod:: make + .. automethod:: __index__ + .. automethod:: _make_data Auxiliary Data diff --git a/docs/source/pcapkit/protocols/application/ftp_data.rst b/docs/source/pcapkit/protocols/application/ftp_data.rst index 423fe3a7b5..b87aa47e82 100644 --- a/docs/source/pcapkit/protocols/application/ftp_data.rst +++ b/docs/source/pcapkit/protocols/application/ftp_data.rst @@ -22,6 +22,8 @@ Transfer Protocol (FTP) [*]_, whose octets it keeps undissected. .. autoproperty:: name + .. automethod:: __index__ + .. rubric:: Footnotes .. [*] https://en.wikipedia.org/wiki/File_Transfer_Protocol diff --git a/docs/source/pcapkit/protocols/application/http.rst b/docs/source/pcapkit/protocols/application/http.rst index 49fe6182dc..5037a7e151 100644 --- a/docs/source/pcapkit/protocols/application/http.rst +++ b/docs/source/pcapkit/protocols/application/http.rst @@ -41,6 +41,7 @@ which tells whether a payload opens with an HTTP/1.* start line. .. automethod:: _make_data + .. automethod:: __index__ .. automethod:: __init__ .. automethod:: __new__ diff --git a/docs/source/pcapkit/protocols/application/ngap.rst b/docs/source/pcapkit/protocols/application/ngap.rst index fcb1cf91c8..f79f8006ec 100644 --- a/docs/source/pcapkit/protocols/application/ngap.rst +++ b/docs/source/pcapkit/protocols/application/ngap.rst @@ -49,6 +49,7 @@ unregistered PPID would, because .. automethod:: make .. automethod:: __length_hint__ + .. automethod:: __index__ .. automethod:: _make_data Decoder Dependency diff --git a/docs/source/pcapkit/protocols/application/tls.rst b/docs/source/pcapkit/protocols/application/tls.rst index 7358334149..03e9232ba9 100644 --- a/docs/source/pcapkit/protocols/application/tls.rst +++ b/docs/source/pcapkit/protocols/application/tls.rst @@ -132,6 +132,7 @@ this is routine. .. automethod:: unpack .. automethod:: __length_hint__ + .. automethod:: __index__ .. automethod:: _make_data diff --git a/docs/source/pcapkit/protocols/link/l2tp.rst b/docs/source/pcapkit/protocols/link/l2tp.rst index ce9a117a2f..764b1b4aa7 100644 --- a/docs/source/pcapkit/protocols/link/l2tp.rst +++ b/docs/source/pcapkit/protocols/link/l2tp.rst @@ -47,9 +47,9 @@ Versions Not Implemented header from v2, and is reachable two ways: over UDP port 1701 like v2, and directly over IP as **protocol number 115**. That second route is why :attr:`Internet.__proto__ ` -leaves 115 unbound: the binding waits on an ``L2TPv3`` class. It would also be -the first member of this family with a real -:meth:`~pcapkit.protocols.protocol.Protocol.__index__`. +leaves 115 unbound: the binding waits on an ``L2TPv3`` class. Protocol 115 names v3 +alone: v2's :meth:`~pcapkit.protocols.protocol.Protocol.__index__` is its UDP +port. **L2F** [:rfc:`2341`] is reached when the version nibble reads ``1``. It is *not* an earlier version of L2TP: :rfc:`2661` ยง3.1 requires ``Ver`` to be 2 and reserves diff --git a/pcapkit/protocols/application/dtls.py b/pcapkit/protocols/application/dtls.py index 308e96295e..8f3e6ecad2 100644 --- a/pcapkit/protocols/application/dtls.py +++ b/pcapkit/protocols/application/dtls.py @@ -759,6 +759,26 @@ def __length_hint__(self) -> 'Literal[13]': """Return an estimated length for the object.""" return _LEGACY_HEADER_LENGTH # type: ignore[return-value] + @classmethod + def __index__(cls) -> 'int': # type: ignore[override] + """Numeral registry index of the protocol. + + Returns: + Numeral registry index of the protocol in `IANA`_ service names and + port numbers: UDP port 5684, the port of + :attr:`UDP.coaps ` as an + :class:`int`, which + :attr:`UDP.__proto__ ` + dispatches here (:ref:`registry-protocol-index`). + + .. _IANA: https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml + + """ + # imported here, as ``import pcapkit`` leaves the UDP registry unloaded (#1538) + from pcapkit.const.reg.apptype.udp import UDP # pylint: disable=import-outside-toplevel + + return int(UDP.coaps) # type: ignore[call-overload] + ########################################################################## # Utilities. ########################################################################## diff --git a/pcapkit/protocols/application/ftp.py b/pcapkit/protocols/application/ftp.py index 984f781798..f5796ef67e 100644 --- a/pcapkit/protocols/application/ftp.py +++ b/pcapkit/protocols/application/ftp.py @@ -266,6 +266,35 @@ def make(self, data=b'%s%s%s%s\r\n' % (prefix, mf, sep, suffix) + b''.join(b'%s\r\n' % line for line in lines), ) + ########################################################################## + # Data models. + ########################################################################## + + @classmethod + def __index__(cls) -> 'int': # type: ignore[override] + """Numeral registry index of the protocol. + + Returns: + Numeral registry index of the protocol in `IANA`_ service names and + port numbers: TCP port 21, the port of + :attr:`TCP.ftp ` as an + :class:`int`, which + :attr:`TCP.__proto__ ` + dispatches here (:ref:`registry-protocol-index`). + + .. _IANA: https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml + + .. versionchanged:: 1.6.0 + + Returns TCP port 21, where it raised + :exc:`~pcapkit.utilities.exceptions.IntError` (:issue:`2627`). + + """ + # imported here, as ``import pcapkit`` leaves the TCP registry unloaded (#1538) + from pcapkit.const.reg.apptype.tcp import TCP # pylint: disable=import-outside-toplevel + + return int(TCP.ftp) # type: ignore[call-overload] + ########################################################################## # Utilities. ########################################################################## diff --git a/pcapkit/protocols/application/ftp_data.py b/pcapkit/protocols/application/ftp_data.py index 0f3e599e2b..e654c90b19 100644 --- a/pcapkit/protocols/application/ftp_data.py +++ b/pcapkit/protocols/application/ftp_data.py @@ -43,10 +43,10 @@ class FTP_DATA(Application, Raw): # pylint: disable=abstract-method Derives from :class:`~pcapkit.protocols.application.application.Application` as well as :class:`~pcapkit.protocols.misc.raw.Raw`, so its layer is - ``'Application'``, where it was :obj:`None`, and its ``__index__`` is + ``'Application'``, where it was :obj:`None`, and its ``__index__`` became :meth:`Application.__index__ `, - raising :exc:`~pcapkit.utilities.exceptions.IntError` where it raised + raising :exc:`~pcapkit.utilities.exceptions.IntError` where it had raised :exc:`~pcapkit.utilities.exceptions.UnsupportedCall` (:issue:`2285`). """ @@ -60,3 +60,32 @@ class FTP_DATA(Application, Raw): # pylint: disable=abstract-method def name(self) -> 'Literal["FTP_DATA"]': # type: ignore[override] """Name of current protocol.""" return 'FTP_DATA' + + ########################################################################## + # Data models. + ########################################################################## + + @classmethod + def __index__(cls) -> 'int': # type: ignore[override] + """Numeral registry index of the protocol. + + Returns: + Numeral registry index of the protocol in `IANA`_ service names and + port numbers: TCP port 20, the port of + :attr:`TCP.ftp_data ` as + an :class:`int`, which + :attr:`TCP.__proto__ ` + dispatches here (:ref:`registry-protocol-index`). + + .. _IANA: https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml + + .. versionchanged:: 1.6.0 + + Returns TCP port 20, where it raised + :exc:`~pcapkit.utilities.exceptions.IntError` (:issue:`2627`). + + """ + # imported here, as ``import pcapkit`` leaves the TCP registry unloaded (#1538) + from pcapkit.const.reg.apptype.tcp import TCP # pylint: disable=import-outside-toplevel + + return int(TCP.ftp_data) # type: ignore[call-overload] diff --git a/pcapkit/protocols/application/http.py b/pcapkit/protocols/application/http.py index ed56d69b64..b46b3fcf4e 100644 --- a/pcapkit/protocols/application/http.py +++ b/pcapkit/protocols/application/http.py @@ -466,6 +466,34 @@ def from_data(cls, data: '_PT | dict[str, Any]', **kwargs: 'Any') -> 'Self': # Data models. ########################################################################## + @classmethod + def __index__(cls) -> 'int': # type: ignore[override] + """Numeral registry index of the protocol. + + Returns: + Numeral registry index of the protocol in `IANA`_ service names and + port numbers: TCP port 80, the port of + :attr:`TCP.http ` as an + :class:`int`, which + :attr:`TCP.__proto__ ` + dispatches here, as it does port 8080 and as + :attr:`UDP.__proto__ ` + does both (:ref:`registry-protocol-index`). The versioned classes + inherit it. + + .. _IANA: https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml + + .. versionchanged:: 1.6.0 + + Returns TCP port 80, where it raised + :exc:`~pcapkit.utilities.exceptions.IntError` (:issue:`2627`). + + """ + # imported here, as ``import pcapkit`` leaves the TCP registry unloaded (#1538) + from pcapkit.const.reg.apptype.tcp import TCP # pylint: disable=import-outside-toplevel + + return int(TCP.http) # type: ignore[call-overload] + def __init__(self, file: 'Optional[IO[bytes] | bytes]' = None, length: 'Optional[int]' = None, *, version: 'Optional[Literal[1, 2]]' = None, **kwargs: 'Any') -> 'None': diff --git a/pcapkit/protocols/application/ngap.py b/pcapkit/protocols/application/ngap.py index df5bb95f3d..0b3f3d3061 100644 --- a/pcapkit/protocols/application/ngap.py +++ b/pcapkit/protocols/application/ngap.py @@ -109,6 +109,7 @@ from pcapkit.const.ngap.procedure_code import ProcedureCode as Enum_ProcedureCode from pcapkit.const.ngap.protocol_ie import ProtocolIE as Enum_ProtocolIE +from pcapkit.const.reg.apptype.sctp import SCTP as Enum_SCTP from pcapkit.corekit.enum import EnumLookup from pcapkit.corekit.infoclass import Info from pcapkit.protocols.application.application import Application @@ -544,6 +545,29 @@ def __length_hint__(self) -> 'Literal[4]': """ return 4 + @classmethod + def __index__(cls) -> 'int': # type: ignore[override] + """Numeral registry index of the protocol. + + Returns: + Numeral registry index of the protocol in `IANA`_ service names and + port numbers: SCTP port 38412, the port of + :attr:`SCTP.ng_control ` + as an :class:`int`. + :attr:`SCTP.__proto__ ` + dispatches by payload protocol identifier rather than by port, so + this value keys no dispatch (:ref:`registry-protocol-index`). + + .. _IANA: https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml + + .. versionchanged:: 1.6.0 + + Returns SCTP port 38412, where it raised + :exc:`~pcapkit.utilities.exceptions.IntError` (:issue:`2627`). + + """ + return int(Enum_SCTP.ng_control) # type: ignore[call-overload] + ########################################################################## # Utilities. ########################################################################## diff --git a/pcapkit/protocols/application/tls.py b/pcapkit/protocols/application/tls.py index 78c9789a46..c710c0746c 100644 --- a/pcapkit/protocols/application/tls.py +++ b/pcapkit/protocols/application/tls.py @@ -357,6 +357,27 @@ def __length_hint__(self) -> 'Literal[5]': """Return an estimated length for the object.""" return _RECORD_HEADER_LENGTH # type: ignore[return-value] + @classmethod + def __index__(cls) -> 'int': # type: ignore[override] + """Numeral registry index of the protocol. + + Returns: + Numeral registry index of the protocol in `IANA`_ service names and + port numbers: TCP port 443, the port of + :attr:`TCP.https_443 ` + as an :class:`int`, which + :attr:`TCP.__proto__ ` + dispatches here (:ref:`registry-protocol-index`). + :class:`~pcapkit.protocols.application.sslv3.SSLv3` inherits it. + + .. _IANA: https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml + + """ + # imported here, as ``import pcapkit`` leaves the TCP registry unloaded (#1538) + from pcapkit.const.reg.apptype.tcp import TCP # pylint: disable=import-outside-toplevel + + return int(TCP.https_443) # type: ignore[call-overload] + ########################################################################## # Utilities. ########################################################################## diff --git a/pcapkit/protocols/link/frad.py b/pcapkit/protocols/link/frad.py index d9a0368870..681b4d4b2b 100644 --- a/pcapkit/protocols/link/frad.py +++ b/pcapkit/protocols/link/frad.py @@ -29,7 +29,7 @@ import collections from typing import TYPE_CHECKING -from pcapkit.const.linux.arphrd import ARPHRD as Enum_ARPHRD +from pcapkit.const.reg.linktype import LinkType as Enum_LinkType from pcapkit.corekit.decorators import beholder from pcapkit.corekit.module import ModuleDescriptor from pcapkit.protocols.data.link.frad import FRAD as Data_FRAD @@ -339,17 +339,21 @@ def __length_hint__(self) -> 'Literal[10]': return 10 @classmethod - def __index__(cls) -> 'Enum_ARPHRD': # pylint: disable=invalid-index-returned + def __index__(cls) -> 'Enum_LinkType': # pylint: disable=invalid-index-returned """Numeral registry index of the protocol. Returns: - Numeral registry index of the protocol in Linux ARP hardware - types, the ``arphrd_type`` that + Numeral registry index of the protocol in `tcpdump`_ link-layer + header types, ``LINKTYPE_FRELAY``: a Q.922 LAPF frame from the + address field on, with no FCS. :attr:`LinuxSLL.__proto__ ` - dispatches here. + names its own key for this class, ``ARPHRD_FRAD`` + (:ref:`registry-protocol-index`). + + .. _tcpdump: https://www.tcpdump.org/linktypes.html """ - return Enum_ARPHRD.FRAD # type: ignore[return-value] + return Enum_LinkType.FRELAY # type: ignore[return-value] ########################################################################## # Utilities. diff --git a/pcapkit/protocols/link/gre.py b/pcapkit/protocols/link/gre.py index d8015d3f3a..dccf27c283 100644 --- a/pcapkit/protocols/link/gre.py +++ b/pcapkit/protocols/link/gre.py @@ -68,8 +68,8 @@ """ from typing import TYPE_CHECKING -from pcapkit.const.linux.arphrd import ARPHRD as Enum_ARPHRD from pcapkit.const.reg.ethertype import EtherType as Enum_EtherType +from pcapkit.const.reg.transtype import TransType as Enum_TransType from pcapkit.protocols.data.link.gre import GRE as Data_GRE from pcapkit.protocols.link.link import Link from pcapkit.protocols.schema.link.gre import GRE as Schema_GRE @@ -354,18 +354,22 @@ def __length_hint__(self) -> 'Literal[16]': return 16 @classmethod - def __index__(cls) -> 'Enum_ARPHRD': # pylint: disable=invalid-index-returned + def __index__(cls) -> 'Enum_TransType': # pylint: disable=invalid-index-returned """Numeral registry index of the protocol. Returns: - Numeral registry index of the protocol in Linux ARP hardware - types: :attr:`~pcapkit.const.linux.arphrd.ARPHRD.IPGRE`, which + Numeral registry index of the protocol in `IANA`_ protocol + numbers. :attr:`LinuxSLL.__proto__ ` - dispatches here alongside - :attr:`~pcapkit.const.linux.arphrd.ARPHRD.IP6GRE`. + names its own keys for this class, + :attr:`~pcapkit.const.linux.arphrd.ARPHRD.IPGRE` and + :attr:`~pcapkit.const.linux.arphrd.ARPHRD.IP6GRE` + (:ref:`registry-protocol-index`). + + .. _IANA: https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml """ - return Enum_ARPHRD.IPGRE # type: ignore[return-value] + return Enum_TransType.GRE # type: ignore[return-value] ########################################################################## # Utilities. diff --git a/pcapkit/protocols/link/l2tp.py b/pcapkit/protocols/link/l2tp.py index 0c0ea62932..6f6db46ca7 100644 --- a/pcapkit/protocols/link/l2tp.py +++ b/pcapkit/protocols/link/l2tp.py @@ -39,8 +39,8 @@ route is why :attr:`Internet.__proto__ ` leaves 115 unbound: the binding waits on an ``L2TPv3`` class, not on a -different framing decision. It also means v3 is the first member of this family -to have a real :meth:`~pcapkit.protocols.protocol.Protocol.__index__`. +different framing decision. It also means 115 names v3 alone: v2's +:meth:`~pcapkit.protocols.protocol.Protocol.__index__` is its UDP port. Binding :class:`~pcapkit.protocols.link.l2tpv2.L2TPv2` at 115 instead does not work (:issue:`548`), and is recorded so it is not proposed again. Over IP the v3 diff --git a/pcapkit/protocols/link/l2tpv2.py b/pcapkit/protocols/link/l2tpv2.py index ce90ae4787..2783b33433 100644 --- a/pcapkit/protocols/link/l2tpv2.py +++ b/pcapkit/protocols/link/l2tpv2.py @@ -58,11 +58,11 @@ from pcapkit.protocols.data.link.l2tp import Flags as Data_Flags from pcapkit.protocols.link.l2tp import L2TP from pcapkit.protocols.schema.link.l2tp import L2TP as Schema_L2TP -from pcapkit.utilities.exceptions import ProtocolError, UnsupportedCall +from pcapkit.utilities.exceptions import ProtocolError if TYPE_CHECKING: from enum import IntEnum as StdlibEnum - from typing import Any, NoReturn, Optional, Type + from typing import Any, Optional, Type from aenum import IntEnum as AenumEnum from typing_extensions import Literal @@ -383,28 +383,31 @@ def __length_hint__(self) -> 'Literal[16]': return 16 @classmethod - def __index__(cls) -> 'NoReturn': # pylint: disable=invalid-index-returned + def __index__(cls) -> 'int': # type: ignore[override] """Numeral registry index of the protocol. - Raises: - UnsupportedCall: This protocol has no registry entry. - - Note: - L2TPv2 is reached through **UDP port 1701**, and a port is not an - ``__index__`` value anywhere in this package -- every non-raising - ``__index__`` returns a :class:`~pcapkit.const.reg.transtype.TransType`, - :class:`~pcapkit.const.reg.ethertype.EtherType`, - :class:`~pcapkit.const.reg.linktype.LinkType` or - :class:`~pcapkit.const.linux.arphrd.ARPHRD`, bar the PCAP frame and - PCAP-NG block instances that return their own index number, and - :meth:`Application.__index__ - ` - raises for exactly this reason. So there is no index to return here. - ``L2TPv3`` will be the first class in this family with one, IANA - protocol number 115. + Returns: + Numeral registry index of the protocol in `IANA`_ service names and + port numbers: UDP port 1701, the port of + :attr:`UDP.l2tp ` as an + :class:`int`, which + :attr:`UDP.__proto__ ` + dispatches here (:ref:`registry-protocol-index`). + :class:`~pcapkit.const.reg.transtype.TransType` 115 names L2TPv3 + over IP instead. + + .. _IANA: https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml + + .. versionchanged:: 1.6.0 + + Returns UDP port 1701, where it raised + :exc:`~pcapkit.utilities.exceptions.UnsupportedCall` (:issue:`2627`). """ - raise UnsupportedCall(f'{cls.__name__!r} object cannot be interpreted as an integer') + # imported here, as ``import pcapkit`` leaves the UDP registry unloaded (#1538) + from pcapkit.const.reg.apptype.udp import UDP # pylint: disable=import-outside-toplevel + + return int(UDP.l2tp) # type: ignore[call-overload] ########################################################################## # Utilities. diff --git a/pcapkit/protocols/link/radiotap.py b/pcapkit/protocols/link/radiotap.py index 0236ae632c..9c3f67be89 100644 --- a/pcapkit/protocols/link/radiotap.py +++ b/pcapkit/protocols/link/radiotap.py @@ -65,7 +65,6 @@ import collections from typing import TYPE_CHECKING -from pcapkit.const.linux.arphrd import ARPHRD as Enum_ARPHRD from pcapkit.const.reg.linktype import LinkType as Enum_LinkType from pcapkit.corekit.module import ModuleDescriptor from pcapkit.protocols.data.link.radiotap import Radiotap as Data_Radiotap @@ -386,17 +385,20 @@ def __length_hint__(self) -> 'Literal[8]': return 8 @classmethod - def __index__(cls) -> 'Enum_ARPHRD': # pylint: disable=invalid-index-returned + def __index__(cls) -> 'Enum_LinkType': # pylint: disable=invalid-index-returned """Numeral registry index of the protocol. Returns: - Numeral registry index of the protocol in Linux ARP hardware - types, the ``arphrd_type`` that + Numeral registry index of the protocol in `tcpdump`_ link-layer + header types. :attr:`LinuxSLL.__proto__ ` - dispatches here. + names its own key for this class, ``ARPHRD_IEEE80211_RADIOTAP`` + (:ref:`registry-protocol-index`). + + .. _tcpdump: https://www.tcpdump.org/linktypes.html """ - return Enum_ARPHRD.IEEE80211_RADIOTAP # type: ignore[return-value] + return Enum_LinkType.IEEE802_11_RADIOTAP # type: ignore[return-value] ########################################################################## # Utilities. diff --git a/tests/protocols/application/test_layer_placement_unit.py b/tests/protocols/application/test_layer_placement_unit.py index 78ec3bb7fc..d2f7ab7744 100644 --- a/tests/protocols/application/test_layer_placement_unit.py +++ b/tests/protocols/application/test_layer_placement_unit.py @@ -149,9 +149,9 @@ def test_ftp_data_subclasses_both_application_and_raw_in_that_order(self) -> Non def test_ftp_data_still_parses_and_rebuilds_as_raw(self) -> None: """The ``Application`` base leaves ``Raw``'s parsing and rebuild untouched.""" + from pcapkit.const.reg.apptype import TCP from pcapkit.protocols.application.ftp_data import FTP_DATA from pcapkit.protocols.misc.no_payload import NoPayload - from pcapkit.utilities.exceptions import IntError packet = FTP_DATA(b'binary-file-contents') self.assertEqual(packet.info.packet, b'binary-file-contents') @@ -160,9 +160,9 @@ def test_ftp_data_still_parses_and_rebuilds_as_raw(self) -> None: self.assertEqual(bytes(FTP_DATA.from_data(packet.info)), b'binary-file-contents') self.assertEqual(FTP_DATA(packet=b'abc').data, b'abc') - # reached by TCP port 20, a port is no index: ``Application``'s refusal - with self.assertRaises(IntError): - FTP_DATA.__index__() + # its own index, TCP port 20, ahead of ``Application``'s refusal (#2627) + self.assertIs(type(FTP_DATA.__index__()), int) + self.assertEqual(int(packet), int(TCP.ftp_data)) def test_link_owns_layer_but_raw_does_not(self) -> None: """The mechanism the ordering convention rests on, asserted directly.""" diff --git a/tests/protocols/application/test_ngap_unit.py b/tests/protocols/application/test_ngap_unit.py index 1cf52773fd..8fe682760a 100644 --- a/tests/protocols/application/test_ngap_unit.py +++ b/tests/protocols/application/test_ngap_unit.py @@ -228,10 +228,10 @@ def test_name_length_and_length_hint(self) -> None: ngap.__header__ = mock.Mock(data=NGSETUP_REQUEST) self.assertEqual(ngap.length, 58) - # An application protocol carries no numeral registry index. - from pcapkit.utilities.exceptions import IntError - with self.assertRaises(IntError): - NGAP.__index__() + # Its index is IANA's ``ng-control`` port, though SCTP dispatches by PPID (#2627). + from pcapkit.const.reg.apptype import SCTP + self.assertIs(type(NGAP.__index__()), int) + self.assertEqual(int(ngap), int(SCTP.ng_control)) def test_exported_from_the_package_namespaces(self) -> None: import pcapkit diff --git a/tests/protocols/link/test_frad_unit.py b/tests/protocols/link/test_frad_unit.py index 9867fe4303..3206dd5aa7 100644 --- a/tests/protocols/link/test_frad_unit.py +++ b/tests/protocols/link/test_frad_unit.py @@ -242,16 +242,18 @@ def test_make_rejects_snap_without_snap_fields(self) -> None: FRAD(nlpid=0x80, snap_oui=b'\x00\x00\x00', snap_pid=0x1_0000) # out of range def test_id_and_index(self) -> None: - """``__index__`` is the ``arphrd_type`` LinuxSLL dispatches here (#2510).""" + """``__index__`` is ``LINKTYPE_FRELAY`` (#2627); LinuxSLL dispatches here + under ``ARPHRD_FRAD``, a key it names itself (#2510). + """ from pcapkit.const.linux.arphrd import ARPHRD + from pcapkit.const.reg.linktype import LinkType from pcapkit.protocols.link.linux_sll import LinuxSLL from pcapkit.protocols.link.frad import FRAD self.assertEqual(FRAD.id(), ('FRAD',)) - self.assertIs(FRAD.__index__(), ARPHRD.FRAD) - self.assertEqual(FRAD.__index__(), 770) - self.assertIs(LinuxSLL._lookup_next_layer(LinuxSLL.__proto__, FRAD.__index__()), - FRAD) + self.assertIs(FRAD.__index__(), LinkType.FRELAY) + self.assertEqual(FRAD.__index__(), 107) + self.assertIs(LinuxSLL._lookup_next_layer(LinuxSLL.__proto__, ARPHRD.FRAD), FRAD) def test_truncated_header_rebuilds_as_captured(self) -> None: """A capture ending inside the header must not blow up the parse, and diff --git a/tests/protocols/link/test_gre_unit.py b/tests/protocols/link/test_gre_unit.py index fb665d4c08..b72abb75ce 100644 --- a/tests/protocols/link/test_gre_unit.py +++ b/tests/protocols/link/test_gre_unit.py @@ -236,16 +236,17 @@ def test_make_rejects_what_does_not_fit(self) -> None: GRE(**kwargs) def test_id_and_index(self) -> None: - """``__index__`` is ``IPGRE``, which LinuxSLL dispatches here alongside - ``IP6GRE`` (#2510). + """``__index__`` is IANA protocol number 47 (#2627); LinuxSLL dispatches + here under ``IPGRE`` and ``IP6GRE``, keys it names itself (#2510). """ from pcapkit.const.linux.arphrd import ARPHRD + from pcapkit.const.reg.transtype import TransType from pcapkit.protocols.link.gre import GRE from pcapkit.protocols.link.linux_sll import LinuxSLL self.assertEqual(GRE.id(), ('GRE',)) - self.assertIs(GRE.__index__(), ARPHRD.IPGRE) - self.assertEqual(GRE.__index__(), 778) + self.assertIs(GRE.__index__(), TransType.GRE) + self.assertEqual(GRE.__index__(), 47) for arphrd in (ARPHRD.IPGRE, ARPHRD.IP6GRE): with self.subTest(arphrd=arphrd): self.assertIs(LinuxSLL._lookup_next_layer(LinuxSLL.__proto__, arphrd), GRE) diff --git a/tests/protocols/link/test_link_unit.py b/tests/protocols/link/test_link_unit.py index 1bc74caac7..4c317b8d3f 100644 --- a/tests/protocols/link/test_link_unit.py +++ b/tests/protocols/link/test_link_unit.py @@ -346,7 +346,8 @@ def test_l2tp_base_is_abstract_and_carries_no_header(self) -> None: self.assertEqual(L2TP.__module__, 'pcapkit.protocols.link.l2tp') self.assertEqual(L2TPv2.__module__, 'pcapkit.protocols.link.l2tpv2') - def test_l2tp_index_is_unsupported_and_make_data_preserves_flags(self) -> None: + def test_l2tpv2_index_is_its_udp_port_and_make_data_preserves_flags(self) -> None: + from pcapkit.const.reg.apptype import UDP from pcapkit.protocols.link.l2tp import L2TP from pcapkit.protocols.link.l2tpv2 import L2TPv2 from pcapkit.utilities.exceptions import UnsupportedCall @@ -364,14 +365,13 @@ def test_l2tp_index_is_unsupported_and_make_data_preserves_flags(self) -> None: ) proto = object.__new__(L2TPv2) - # Neither the abstract base nor v2 has a numeral registry index: v2 is - # reached by UDP port 1701, and a port is not an ``__index__`` value - # anywhere in this package. An L2TPv3 class would be the first with one - # (IP protocol 115). - for klass in (L2TP, L2TPv2): - with self.subTest(protocol=klass.__name__): - with self.assertRaises(UnsupportedCall): - klass.__index__() + # The abstract base has no numeral registry index; v2's is UDP port 1701, + # where UDP dispatches it (#2627). IP protocol 115 names L2TPv3 instead. + with self.assertRaises(UnsupportedCall): + L2TP.__index__() + self.assertIs(type(L2TPv2.__index__()), int) + self.assertEqual(int(proto), int(UDP.l2tp)) + self.assertEqual(int(proto), 1701) self.assertEqual(proto.__length_hint__(), 16) values = L2TPv2._make_data(data) self.assertEqual(values['type'], True) diff --git a/tests/protocols/link/test_linux_sll_arphrd_registry_unit.py b/tests/protocols/link/test_linux_sll_arphrd_registry_unit.py index 6ede1a0671..92d7a8a029 100644 --- a/tests/protocols/link/test_linux_sll_arphrd_registry_unit.py +++ b/tests/protocols/link/test_linux_sll_arphrd_registry_unit.py @@ -5,7 +5,7 @@ Covers :attr:`LinuxSLL.__proto__ ` as shipped, dispatch through it and through the EtherType path from both versions, a parse stopping at either version on either path (#2532), -the ``__index__`` of the classes it reaches, and the registrars of it and of +the keys of the classes it reaches, and the registrars of it and of :class:`~pcapkit.protocols.link.frad.FRAD`'s and :class:`~pcapkit.protocols.link.netlink.Netlink`'s registries. @@ -208,7 +208,11 @@ def test_a_stop_at_another_protocol_still_dispatches_here(self) -> None: self.assertEqual(version(raw, len(raw), **stop).protochain.chain, f'{alias}:IPv4:UDP') - def test_index_is_the_arphrd_type_that_dispatches_here(self) -> None: + def test_each_arphrd_type_dispatches_to_its_class(self) -> None: + """Each key is spelled out, not read off ``__index__``, which returns + the class's LinkType or TransType value (#2627), bar Netlink's, which + is ``ARPHRD_NETLINK``. + """ from pcapkit.const.linux.arphrd import ARPHRD from pcapkit.protocols.link.frad import FRAD from pcapkit.protocols.link.gre import GRE @@ -216,13 +220,13 @@ def test_index_is_the_arphrd_type_that_dispatches_here(self) -> None: from pcapkit.protocols.link.netlink import Netlink from pcapkit.protocols.link.radiotap import Radiotap - for klass, member in ((FRAD, ARPHRD.FRAD), (GRE, ARPHRD.IPGRE), - (Radiotap, ARPHRD.IEEE80211_RADIOTAP), (Netlink, ARPHRD.NETLINK)): - with self.subTest(protocol=klass.__name__): - self.assertIs(klass.__index__(), member) - self.assertIs(LinuxSLL._lookup_next_layer(LinuxSLL.__proto__, klass.__index__()), - klass) - self.assertIs(LinuxSLL._lookup_next_layer(LinuxSLL.__proto__, ARPHRD.IP6GRE), GRE) + for klass, member in ((FRAD, ARPHRD.FRAD), (GRE, ARPHRD.IPGRE), (GRE, ARPHRD.IP6GRE), + (Radiotap, ARPHRD.IEEE80211_RADIOTAP)): + with self.subTest(protocol=klass.__name__, arphrd=int(member)): + self.assertIsNot(klass.__index__(), member) + self.assertIs(LinuxSLL._lookup_next_layer(LinuxSLL.__proto__, member), klass) + self.assertIs(Netlink.__index__(), ARPHRD.NETLINK) + self.assertIs(LinuxSLL._lookup_next_layer(LinuxSLL.__proto__, ARPHRD.NETLINK), Netlink) ########################################################################## # The registrars. diff --git a/tests/protocols/link/test_radiotap_unit.py b/tests/protocols/link/test_radiotap_unit.py index 3bb2d4db21..53630261a5 100644 --- a/tests/protocols/link/test_radiotap_unit.py +++ b/tests/protocols/link/test_radiotap_unit.py @@ -383,15 +383,19 @@ def test_register_radiotap_rejects_a_code_of_the_wrong_kind(self) -> None: self.assertEqual(dict(Radiotap.__proto__), before) def test_id_and_index(self) -> None: - """``__index__`` is the ``arphrd_type`` LinuxSLL dispatches here (#2510).""" + """``__index__`` is ``LINKTYPE_IEEE802_11_RADIOTAP`` (#2627); LinuxSLL + dispatches here under ``ARPHRD_IEEE80211_RADIOTAP``, a key it names + itself (#2510). + """ from pcapkit.const.linux.arphrd import ARPHRD + from pcapkit.const.reg.linktype import LinkType from pcapkit.protocols.link.linux_sll import LinuxSLL from pcapkit.protocols.link.radiotap import Radiotap self.assertEqual(Radiotap.id(), ('Radiotap',)) - self.assertIs(Radiotap.__index__(), ARPHRD.IEEE80211_RADIOTAP) - self.assertEqual(Radiotap.__index__(), 803) - self.assertIs(LinuxSLL._lookup_next_layer(LinuxSLL.__proto__, Radiotap.__index__()), + self.assertIs(Radiotap.__index__(), LinkType.IEEE802_11_RADIOTAP) + self.assertEqual(Radiotap.__index__(), 127) + self.assertIs(LinuxSLL._lookup_next_layer(LinuxSLL.__proto__, ARPHRD.IEEE80211_RADIOTAP), Radiotap) diff --git a/tests/protocols/test_dispatch_reachability_unit.py b/tests/protocols/test_dispatch_reachability_unit.py index d18792effe..69519daef2 100644 --- a/tests/protocols/test_dispatch_reachability_unit.py +++ b/tests/protocols/test_dispatch_reachability_unit.py @@ -19,6 +19,11 @@ #2392), so the audit and the registration mechanism cannot drift apart: an enum type that declares one gets audited here the same day. +An index is the class's own LinkType, EtherType or TransType value, or AppType +port, bar Netlink's ARPHRD type (#2627), whether or not anything dispatches it +there, so a claim nothing reaches is an entry in :data:`UNBOUND` giving the +reason, checked in both directions. + Nothing is parsed and no capture under :file:`examples/captures/` is read, so this belongs to the unit tier. @@ -42,6 +47,17 @@ RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) +#: Claims nothing reaches, as ``(protocol, destination, found)``: why. Binding +#: one changes parsed output, and deletes its entry in the same change (#2649). +UNBOUND = { + ('pcapkit.protocols.link.frad.FRAD', 'Frame', None): + 'LINKTYPE_FRELAY (107) is the Q.922 frame FRAD reads; Frame.__proto__ binds no 107', + ('pcapkit.protocols.link.frad.FRAD', 'PCAPNG', None): + 'LINKTYPE_FRELAY (107) is the Q.922 frame FRAD reads; PCAPNG.__proto__ binds no 107', + ('pcapkit.protocols.link.gre.GRE', 'Internet', None): + 'IP protocol 47 is GRE; Internet.__proto__ binds no 47', +} # type: dict[tuple[str, str, Any], str] + class Gap(NamedTuple): """One class that declares a code it cannot be reached under.""" @@ -156,17 +172,30 @@ def test_every_indexed_class_is_reachable_under_the_code_it_declares(self) -> No """The invariant itself, over every class rather than a hand-picked few.""" gaps, checked = audit() - self.assertEqual(gaps, [], 'protocol classes declare codes nothing dispatches: ' - + '; '.join(f'{gap.protocol} says {gap.code!r} but ' - f'{gap.destination}.__proto__ holds ' - f'{gap.found!r}' for gap in gaps)) + unrecorded = [gap for gap in gaps + if (gap.protocol, gap.destination, gap.found) not in UNBOUND] + self.assertEqual(unrecorded, [], 'protocol classes declare codes nothing dispatches: ' + + '; '.join(f'{gap.protocol} says {gap.code!r} but ' + f'{gap.destination}.__proto__ holds ' + f'{gap.found!r}' for gap in unrecorded)) # A walk that found nothing to check would also report no gaps, so pin # 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) + # ``PCAPNG`` for a ``LinkType`` code, ``Internet`` for a ``TransType`` + # one, ``Link`` for an ``EtherType`` one, ``LinuxSLL`` for Netlink's + # ``ARPHRD`` one -- less the ``UNBOUND`` entries, with ``InARP`` and + # ``DRARP`` reaching their family bases' entries. + self.assertGreaterEqual(checked, 41) + + def test_unbound_entries_are_gaps_the_audit_finds(self) -> None: + """The other direction: an entry whose claim is reached, or whose class, + destination or holder moved, fails until it is deleted or rewritten. + """ + gaps, _ = audit() + + found = {(gap.protocol, gap.destination, gap.found) for gap in gaps} + self.assertEqual(sorted(set(UNBOUND) - found, key=repr), []) 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. @@ -213,11 +242,12 @@ def test_transtype_l2tp_is_unbound_because_v3_has_no_class(self) -> None: IANA-assigned IP protocol ID 115."* So 115 names the v3-over-IP header, which no class in this tree implements -- and this audit deliberately does **not** flag it, because nothing claims 115 in the first place. - :class:`~pcapkit.protocols.link.l2tpv2.L2TPv2` raises from - ``__index__`` rather than returning it. + :class:`~pcapkit.protocols.link.l2tpv2.L2TPv2`'s ``__index__`` returns + its UDP port rather than 115 (#2627), and the family base raises. """ import pcapkit.protocols.link as linkpkg + from pcapkit.const.reg.apptype import UDP from pcapkit.const.reg.transtype import TransType from pcapkit.protocols.internet.internet import Internet from pcapkit.protocols.link.l2tp import L2TP @@ -225,10 +255,9 @@ def test_transtype_l2tp_is_unbound_because_v3_has_no_class(self) -> None: from pcapkit.utilities.exceptions import UnsupportedCall self.assertNotIn(TransType.L2TP, Internet.__proto__) - for cls in (L2TP, L2TPv2): - with self.subTest(cls=cls.__name__): - with self.assertRaises(UnsupportedCall): - cls.__index__() + with self.assertRaises(UnsupportedCall): + L2TP.__index__() + self.assertEqual(L2TPv2.__index__(), int(UDP.l2tp)) modules = {info.name for info in pkgutil.iter_modules(linkpkg.__path__)} self.assertNotIn('l2tpv3', modules) diff --git a/tests/protocols/test_protocol_index_registry_2627_unit.py b/tests/protocols/test_protocol_index_registry_2627_unit.py new file mode 100644 index 0000000000..b3496978bf --- /dev/null +++ b/tests/protocols/test_protocol_index_registry_2627_unit.py @@ -0,0 +1,258 @@ +# -*- coding: utf-8 -*- +"""A protocol's ``__index__`` is its LinkType, EtherType, TransType or AppType value (#2627). + +The owner ruled on #2627 that ``__index__`` returns the protocol's member of one of +those registries wherever one names it, ahead of any other registry's code such as +an ARPHRD type, bar the classes in :data:`EXCEPTIONS`. An AppType member is a +:class:`str`, which ``__index__`` may not return, so an AppType-valued index is +the member's port as an :class:`int`. :data:`EXPECTED` derives each class's value +from the registry and the number it assigns, never from ``__index__``, and the +tests hold every protocol class to the rule in both directions: a class in the +table returns that value, and a class returning a member of one of those +registries is in the table. A class in neither table returns :data:`None` or +raises. + +This module is unit-tier and reads no capture. + +""" +from __future__ import annotations + +import importlib +import importlib.util +import operator +import pkgutil +import unittest +from typing import TYPE_CHECKING + +from tests._support import reimport_once_per_class + +if TYPE_CHECKING: + from typing import Any + +#: Test tier (#1958): a sweep over every protocol class, under its index. +__tier__ = 'LATEST' + +RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') +HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) + +LINKTYPE = 'pcapkit.const.reg.linktype.LinkType' +ETHERTYPE = 'pcapkit.const.reg.ethertype.EtherType' +TRANSTYPE = 'pcapkit.const.reg.transtype.TransType' +APP_TCP = 'pcapkit.const.reg.apptype.tcp.TCP' +APP_UDP = 'pcapkit.const.reg.apptype.udp.UDP' +APP_SCTP = 'pcapkit.const.reg.apptype.sctp.SCTP' +ARPHRD = 'pcapkit.const.linux.arphrd.ARPHRD' + +#: The AppType registries, whose member a class's index gives as its port. +APPTYPES = (APP_TCP, APP_UDP, APP_SCTP) + +#: Every protocol class one of those registries names, as ``class: (registry, +#: number)``: tcpdump.org's LINKTYPE value, IEEE's EtherType, IANA's protocol +#: number, or the IANA port the class is bound at. +EXPECTED = { + 'pcapkit.protocols.application.drarp.DRARP': (ETHERTYPE, 0x8035), + 'pcapkit.protocols.application.dtls.DTLS': (APP_UDP, 5684), + 'pcapkit.protocols.application.ftp.FTP': (APP_TCP, 21), + 'pcapkit.protocols.application.ftp_data.FTP_DATA': (APP_TCP, 20), + 'pcapkit.protocols.application.http.HTTP': (APP_TCP, 80), + 'pcapkit.protocols.application.httpv1.HTTPv1': (APP_TCP, 80), + 'pcapkit.protocols.application.httpv2.HTTPv2': (APP_TCP, 80), + 'pcapkit.protocols.application.ngap.NGAP': (APP_SCTP, 38412), + 'pcapkit.protocols.application.ospf.OSPF': (TRANSTYPE, 89), + 'pcapkit.protocols.application.rarp.RARP': (ETHERTYPE, 0x8035), + 'pcapkit.protocols.application.sslv3.SSLv3': (APP_TCP, 443), + 'pcapkit.protocols.application.tls.TLS': (APP_TCP, 443), + 'pcapkit.protocols.internet.ah.AH': (TRANSTYPE, 51), + 'pcapkit.protocols.internet.esp.ESP': (TRANSTYPE, 50), + 'pcapkit.protocols.internet.hip.HIP': (TRANSTYPE, 139), + 'pcapkit.protocols.internet.hopopt.HOPOPT': (TRANSTYPE, 0), + 'pcapkit.protocols.internet.icmpv4.ICMPv4': (TRANSTYPE, 1), + 'pcapkit.protocols.internet.icmpv6.ICMPv6': (TRANSTYPE, 58), + 'pcapkit.protocols.internet.igmp.IGMP': (TRANSTYPE, 2), + 'pcapkit.protocols.internet.igmpv1.IGMPv1': (TRANSTYPE, 2), + 'pcapkit.protocols.internet.igmpv2.IGMPv2': (TRANSTYPE, 2), + 'pcapkit.protocols.internet.igmpv3.IGMPv3': (TRANSTYPE, 2), + 'pcapkit.protocols.internet.ipv4.IPv4': (TRANSTYPE, 4), + 'pcapkit.protocols.internet.ipv6.IPv6': (TRANSTYPE, 41), + 'pcapkit.protocols.internet.ipv6_frag.IPv6_Frag': (TRANSTYPE, 44), + 'pcapkit.protocols.internet.ipv6_opts.IPv6_Opts': (TRANSTYPE, 60), + 'pcapkit.protocols.internet.ipv6_route.IPv6_Route': (TRANSTYPE, 43), + 'pcapkit.protocols.internet.ipx.IPX': (TRANSTYPE, 111), + 'pcapkit.protocols.internet.mh.MH': (TRANSTYPE, 135), + 'pcapkit.protocols.link.arp.ARP': (ETHERTYPE, 0x0806), + 'pcapkit.protocols.link.c_tag.C_Tag': (ETHERTYPE, 0x8100), + 'pcapkit.protocols.link.ethernet.Ethernet': (LINKTYPE, 1), + 'pcapkit.protocols.link.frad.FRAD': (LINKTYPE, 107), + 'pcapkit.protocols.link.gre.GRE': (TRANSTYPE, 47), + 'pcapkit.protocols.link.ieee802_11.IEEE802_11': (LINKTYPE, 105), + 'pcapkit.protocols.link.inarp.InARP': (ETHERTYPE, 0x0806), + 'pcapkit.protocols.link.l2tpv2.L2TPv2': (APP_UDP, 1701), + 'pcapkit.protocols.link.linux_sll1.LinuxSLL1': (LINKTYPE, 113), + 'pcapkit.protocols.link.linux_sll2.LinuxSLL2': (LINKTYPE, 276), + 'pcapkit.protocols.link.loopback.Loopback': (LINKTYPE, 0), + 'pcapkit.protocols.link.radiotap.Radiotap': (LINKTYPE, 127), + 'pcapkit.protocols.link.s_tag.S_Tag': (ETHERTYPE, 0x88A8), + 'pcapkit.protocols.transport.dccp.DCCP': (TRANSTYPE, 33), + 'pcapkit.protocols.transport.sctp.SCTP': (TRANSTYPE, 132), + 'pcapkit.protocols.transport.tcp.TCP': (TRANSTYPE, 6), + 'pcapkit.protocols.transport.udp.UDP': (TRANSTYPE, 17), +} + +#: Classes that keep another registry's code, in the same form. Netlink keeps +#: ARPHRD_NETLINK, as LINKTYPE_NETLINK (253) names the SLL-framed form (#2627). +EXCEPTIONS = { + 'pcapkit.protocols.link.netlink.Netlink': (ARPHRD, 824), +} + + +def _load(path: 'str') -> 'Any': + """The object a dotted ``module.Name`` path names.""" + module, _, name = path.rpartition('.') + return getattr(importlib.import_module(module), name) + + +def _expected(path: 'str') -> 'Any': + """The registry member :data:`EXPECTED` names for the class at ``path``.""" + registry, number = EXPECTED[path] + return _load(registry).get(number) + + +def _protocols() -> 'list[type]': + """Every :class:`~pcapkit.protocols.protocol.ProtocolBase` descendant, sorted.""" + import pcapkit.protocols as protopkg + from pcapkit.protocols.protocol import ProtocolBase + + # Import every protocol module, or ``__subclasses__`` sees only whatever this + # process happened to touch first. + for info in pkgutil.walk_packages(protopkg.__path__, prefix='pcapkit.protocols.'): + importlib.import_module(info.name) + + found = set() # type: set[type] + stack = [ProtocolBase] # type: list[type] + while stack: + for sub in stack.pop().__subclasses__(): + if sub not in found: + found.add(sub) + stack.append(sub) + return sorted(found, key=lambda cls: (cls.__module__, cls.__qualname__)) + + +def _index(cls: 'Any') -> 'Any': + """``cls.__index__()``, or the exception it raises.""" + try: + return cls.__index__() + except Exception as exc: # pylint: disable=broad-except + return exc + + +def _instances() -> 'dict[str, Any]': + """An instance of each AppType-valued class in :data:`EXPECTED`, by its path.""" + from pcapkit.const.http.frame import Frame + from pcapkit.protocols.application.dtls import DTLS + from pcapkit.protocols.application.ftp import FTP + from pcapkit.protocols.application.ftp_data import FTP_DATA + from pcapkit.protocols.application.http import HTTP + from pcapkit.protocols.application.httpv1 import HTTPv1 + from pcapkit.protocols.application.httpv2 import HTTPv2 + from pcapkit.protocols.application.ngap import NGAP + from pcapkit.protocols.application.sslv3 import SSLv3 + from pcapkit.protocols.application.tls import TLS + from pcapkit.protocols.link.l2tpv2 import L2TPv2 + + # a ChangeCipherSpec record of each, under legacy version 0x0303, 0x0300 and 0xFEFD + tls, sslv3 = b'\x14\x03\x03\x00\x01\x01', b'\x14\x03\x00\x00\x01\x01' + dtls = b'\x14\xfe\xfd\x00\x00' + (1).to_bytes(6, 'big') + b'\x00\x01\x01' + + return { + 'pcapkit.protocols.application.dtls.DTLS': DTLS(dtls), + 'pcapkit.protocols.application.ftp.FTP': FTP(b'USER anonymous\r\n'), + 'pcapkit.protocols.application.ftp_data.FTP_DATA': FTP_DATA(b'abc'), + 'pcapkit.protocols.application.http.HTTP': HTTP(b'GET / HTTP/1.1\r\n\r\n'), + 'pcapkit.protocols.application.httpv1.HTTPv1': HTTPv1(method='GET', uri='/', + http_version='1.1'), + 'pcapkit.protocols.application.httpv2.HTTPv2': HTTPv2(type=Frame.DATA, sid=1, + frame={'data': b'x'}), + # unparsed, since decoding NGAP needs the optional pycrate + 'pcapkit.protocols.application.ngap.NGAP': object.__new__(NGAP), + 'pcapkit.protocols.application.sslv3.SSLv3': SSLv3(sslv3), + 'pcapkit.protocols.application.tls.TLS': TLS(tls), + 'pcapkit.protocols.link.l2tpv2.L2TPv2': L2TPv2(version=2, tunnel_id=1, session_id=2), + } + + +@unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') +class ProtocolIndexRegistryTests(unittest.TestCase): + """Every protocol class's index is its own standard registry value.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def _standard(self) -> 'tuple[type, ...]': + from pcapkit.const.reg.apptype import AppType + from pcapkit.const.reg.ethertype import EtherType + from pcapkit.const.reg.linktype import LinkType + from pcapkit.const.reg.transtype import TransType + + return (LinkType, EtherType, TransType, AppType) + + def test_each_class_returns_the_value_its_registry_assigns(self) -> None: + for path, (registry, number) in EXPECTED.items(): + with self.subTest(protocol=path): + member = _expected(path) + self.assertEqual(int(member), number) + if registry in APPTYPES: + self.assertIs(type(_index(_load(path))), int) + self.assertEqual(_index(_load(path)), number) + else: + self.assertIs(_index(_load(path)), member) + + def test_each_exception_returns_its_own_registrys_member(self) -> None: + for path, (registry, number) in EXCEPTIONS.items(): + with self.subTest(protocol=path): + member = _load(registry).get(number) + self.assertEqual(int(member), number) + self.assertIs(_index(_load(path)), member) + + def test_int_and_operator_index_give_an_apptype_valued_instances_port(self) -> None: + instances = _instances() + self.assertEqual(sorted(instances), + sorted(path for path, (registry, _) in EXPECTED.items() + if registry in APPTYPES)) + for path, instance in instances.items(): + with self.subTest(protocol=path): + self.assertIsInstance(instance, _load(path)) + self.assertEqual(int(instance), EXPECTED[path][1]) + self.assertEqual(operator.index(instance), EXPECTED[path][1]) + + def test_every_class_returning_a_standard_value_is_in_the_table(self) -> None: + standard = self._standard() + stray = [f'{cls.__module__}.{cls.__qualname__}' for cls in _protocols() + if isinstance(_index(cls), standard) + and f'{cls.__module__}.{cls.__qualname__}' not in EXPECTED] + self.assertEqual(stray, []) + + def test_no_class_returns_another_registrys_code(self) -> None: + """Outside both tables an index is :data:`None` or raises, never an + ARPHRD type, NLPID or other secondary code. + """ + standard = self._standard() + other = {f'{cls.__module__}.{cls.__qualname__}': repr(value) for cls in _protocols() + if f'{cls.__module__}.{cls.__qualname__}' not in {**EXPECTED, **EXCEPTIONS} + for value in (_index(cls),) + if not (value is None or isinstance(value, (Exception,) + standard))} + self.assertEqual(other, {}) + + def test_a_member_sharing_its_bases_value_inherits_it(self) -> None: + """A family member whose value is its base's defines no ``__index__``.""" + for path, value in EXPECTED.items(): + cls = _load(path) + for base in cls.__mro__[1:]: + name = f'{base.__module__}.{base.__qualname__}' + if EXPECTED.get(name) == value: + with self.subTest(protocol=path, base=name): + self.assertNotIn('__index__', vars(cls)) + break + + +if __name__ == '__main__': + unittest.main()