From 039216bdf8196bad3169ea7a276dcd2f26cda28f Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Sun, 25 Oct 2026 23:48:04 -0400 Subject: [PATCH] fix(foundation): reject a registrar code from the wrong enum (#2224) Problem: _check_code only required some enum member. The registry enums compare equal to their values, so a member of another enum was stored and could replace a shipped entry, e.g. register_linktype(TransType.ICMP, ...) overwrote Frame.__proto__[LinkType.ETHERNET]. Fix: _check_code takes the enum the registrar is typed on and checks the type, not equality. register_tcp, register_udp, register_sctp and register_apptype call it too, with number=True, so a plain int is still accepted but an IntEnum member of another enum is not. Test: test_registrar_code_check_unit gains a wrong-enum case for every registrar, each equal by value to one of its own members where possible, asserting RegistryError with every registry snapshotted and unchanged. --- pcapkit/foundation/registry/protocols.py | 184 ++++++++++++------ .../test_registrar_code_check_unit.py | 98 +++++++++- 2 files changed, 216 insertions(+), 66 deletions(-) diff --git a/pcapkit/foundation/registry/protocols.py b/pcapkit/foundation/registry/protocols.py index 780e68dfc6..16b686a742 100644 --- a/pcapkit/foundation/registry/protocols.py +++ b/pcapkit/foundation/registry/protocols.py @@ -20,6 +20,18 @@ import aenum +from pcapkit.const.hip.parameter import Parameter as HIP_Parameter +from pcapkit.const.http.frame import Frame as HTTP_Frame +from pcapkit.const.ipv4.option_number import OptionNumber as IPv4_OptionNumber +from pcapkit.const.ipv6.option import Option as IPv6_Option +from pcapkit.const.ipv6.routing import Routing as IPv6_Routing +from pcapkit.const.mh.cga_extension import CGAExtension as MH_CGAExtension +from pcapkit.const.mh.option import Option as MH_Option +from pcapkit.const.mh.packet import Packet as MH_Packet +from pcapkit.const.pcapng.block_type import BlockType as PCAPNG_BlockType +from pcapkit.const.pcapng.option_type import OptionType as PCAPNG_OptionType +from pcapkit.const.pcapng.record_type import RecordType as PCAPNG_RecordType +from pcapkit.const.pcapng.secrets_type import SecretsType as PCAPNG_SecretsType from pcapkit.const.reg.apptype import AppType as Enum_AppType from pcapkit.const.reg.apptype import TransportProtocol from pcapkit.const.reg.ethertype import EtherType as Enum_EtherType @@ -27,6 +39,8 @@ from pcapkit.const.reg.transtype import TransType as Enum_TransType from pcapkit.const.sctp.payload_protocol_identifier import \ PayloadProtocolIdentifier as Enum_PayloadProtocolIdentifier +from pcapkit.const.tcp.mp_tcp_option import MPTCPOption as TCP_MPTCPOption +from pcapkit.const.tcp.option import Option as TCP_Option from pcapkit.corekit.module import ModuleDescriptor from pcapkit.corekit.sentinels import NULL from pcapkit.protocols import __proto__ as protocol_registry @@ -69,25 +83,11 @@ if TYPE_CHECKING: from typing import Any, Iterator, Optional, Type - from pcapkit.const.hip.parameter import Parameter as HIP_Parameter - from pcapkit.const.http.frame import Frame as HTTP_Frame - from pcapkit.const.ipv4.option_number import OptionNumber as IPv4_OptionNumber - from pcapkit.const.ipv6.option import Option as IPv6_Option - from pcapkit.const.ipv6.routing import Routing as IPv6_Routing - from pcapkit.const.mh.cga_extension import CGAExtension as MH_CGAExtension - from pcapkit.const.mh.option import Option as MH_Option - from pcapkit.const.mh.packet import Packet as MH_Packet - from pcapkit.const.pcapng.block_type import BlockType as PCAPNG_BlockType - from pcapkit.const.pcapng.option_type import OptionType as PCAPNG_OptionType - from pcapkit.const.pcapng.record_type import RecordType as PCAPNG_RecordType - from pcapkit.const.pcapng.secrets_type import SecretsType as PCAPNG_SecretsType from pcapkit.const.reg.ethertype import EtherType from pcapkit.const.reg.linktype import LinkType from pcapkit.const.reg.transtype import TransType from pcapkit.const.sctp.payload_protocol_identifier import \ PayloadProtocolIdentifier as SCTP_PayloadProtocolIdentifier - from pcapkit.const.tcp.mp_tcp_option import MPTCPOption as TCP_MPTCPOption - from pcapkit.const.tcp.option import Option as TCP_Option from pcapkit.corekit.sentinels import NullType from pcapkit.protocols.application.httpv2 import FrameConstructor as HTTP_FrameConstructor from pcapkit.protocols.application.httpv2 import FrameParser as HTTP_FrameParser @@ -536,21 +536,38 @@ def register_protocol_code(protocol: 'Type[ProtocolBase]', code: 'Any') -> 'None destination.__name__, key) -def _check_code(code: 'Any') -> 'None': - """Reject a registration key that is not an enum member. +def _check_code(code: 'Any', kind: 'type', *, number: 'bool' = False) -> 'None': + """Reject a registration key that is not a member of the registrar's enum. - The registrars typed on an enum key call this before touching any - registry, so a bad ``code`` leaves every registry as it was (:issue:`2186`). + Each registrar typed on one enum calls this before touching any registry, + so a bad ``code`` leaves every registry as it was (:issue:`2186`). The check is on the type, + not the value: the registry enums compare equal to their values, so a + member of another enum would key the entry of the member it equals, e.g. + :attr:`TransType.ICMP ` would + replace :attr:`LinkType.ETHERNET ` + (:issue:`2224`). Args: code: Registration key handed to the registrar. + kind: Enum the registrar is typed on. + number: Whether a bare :class:`int` is a valid key too, as for the + registrars keyed on a port number or a payload protocol identifier. Raises: - RegistryError: If ``code`` is not an enum member. + RegistryError: If ``code`` is not a member of ``kind`` and, with + ``number``, not an :class:`int` either. A member of another enum is + rejected even where an :class:`int` is accepted. """ - if not isinstance(code, (enum.Enum, aenum.Enum)): - raise RegistryError(f'code must be an enum member, not {code!r}') + if isinstance(code, kind): + return + member = isinstance(code, (enum.Enum, aenum.Enum)) + if number and isinstance(code, int) and not member: + return + expected = f'an int or a member of {kind.__name__}' if number else f'a member of {kind.__name__}' + if member: + raise RegistryError(f'code must be {expected}, not a member of {type(code).__name__}: {code!r}') + raise RegistryError(f'code must be {expected}, not {code!r}') ############################################################################### @@ -589,12 +606,13 @@ def register_linktype(code: 'LinkType', module: 'str | ModuleDescriptor[Protocol * :func:`pcapkit.foundation.registry.protocols.register_pcapng` Raises: - RegistryError: If ``code`` is not an enum member, or ``module`` + RegistryError: If ``code`` is not a :class:`~pcapkit.const.reg.linktype.LinkType` + member, e.g. a member of another enum, or ``module`` is not a :class:`~pcapkit.protocols.protocol.ProtocolBase` subclass. """ - _check_code(code) + _check_code(code, Enum_LinkType) if isinstance(module, str): module = cast('ModuleDescriptor[ProtocolBase]', ModuleDescriptor(module, class_)) @@ -633,12 +651,13 @@ def register_pcap(code: 'LinkType', module: 'str | ModuleDescriptor[ProtocolBase class\_: class name Raises: - RegistryError: If ``code`` is not an enum member, or ``module`` + RegistryError: If ``code`` is not a :class:`~pcapkit.const.reg.linktype.LinkType` + member, e.g. a member of another enum, or ``module`` is not a :class:`~pcapkit.protocols.protocol.ProtocolBase` subclass. """ - _check_code(code) + _check_code(code, Enum_LinkType) if isinstance(module, str): module = cast('ModuleDescriptor[ProtocolBase]', ModuleDescriptor(module, class_)) @@ -676,12 +695,13 @@ def register_pcapng(code: 'LinkType', module: 'str | ModuleDescriptor[ProtocolBa class\_: class name Raises: - RegistryError: If ``code`` is not an enum member, or ``module`` + RegistryError: If ``code`` is not a :class:`~pcapkit.const.reg.linktype.LinkType` + member, e.g. a member of another enum, or ``module`` is not a :class:`~pcapkit.protocols.protocol.ProtocolBase` subclass. """ - _check_code(code) + _check_code(code, Enum_LinkType) if isinstance(module, str): module = cast('ModuleDescriptor[ProtocolBase]', ModuleDescriptor(module, class_)) @@ -724,12 +744,13 @@ def register_ethertype(code: 'EtherType', module: 'str | ModuleDescriptor[Protoc class\_: class name Raises: - RegistryError: If ``code`` is not an enum member, or ``module`` + RegistryError: If ``code`` is not a :class:`~pcapkit.const.reg.ethertype.EtherType` + member, e.g. a member of another enum, or ``module`` is not a :class:`~pcapkit.protocols.protocol.ProtocolBase` subclass. """ - _check_code(code) + _check_code(code, Enum_EtherType) if isinstance(module, str): module = cast('ModuleDescriptor[ProtocolBase]', ModuleDescriptor(module, class_)) @@ -772,12 +793,13 @@ def register_transtype(code: 'TransType', module: 'str | ModuleDescriptor[Protoc class\_: class name Raises: - RegistryError: If ``code`` is not an enum member, or ``module`` + RegistryError: If ``code`` is not a :class:`~pcapkit.const.reg.transtype.TransType` + member, e.g. a member of another enum, or ``module`` is not a :class:`~pcapkit.protocols.protocol.ProtocolBase` subclass. """ - _check_code(code) + _check_code(code, Enum_TransType) if isinstance(module, str): module = cast('ModuleDescriptor[ProtocolBase]', ModuleDescriptor(module, class_)) @@ -809,11 +831,12 @@ def register_ipv4_option(code: 'IPv4_OptionNumber', meth: 'str | tuple[IPv4_Opti It should be a subclass of :class:`pcapkit.protocols.schema.internet.ipv4.Option`. Raises: - RegistryError: If ``code`` is not an enum member, or ``meth`` + RegistryError: If ``code`` is not a :class:`~pcapkit.const.ipv4.option_number.OptionNumber` + member, e.g. a member of another enum, or ``meth`` is a name with no matching ``_read_opt_*`` method. """ - _check_code(code) + _check_code(code, IPv4_OptionNumber) if isinstance(meth, str) and not hasattr(IPv4, f'_read_opt_{meth}'): raise RegistryError('method must be a valid IPv4 option parser function') @@ -842,11 +865,12 @@ def register_hip_parameter(code: 'HIP_Parameter', meth: 'str | tuple[HIP_Paramet It should be a subclass of :class:`pcapkit.protocols.schema.internet.hip.Parameter`. Raises: - RegistryError: If ``code`` is not an enum member, or ``meth`` + RegistryError: If ``code`` is not a :class:`~pcapkit.const.hip.parameter.Parameter` + member, e.g. a member of another enum, or ``meth`` is a name with no matching ``_read_param_*`` method. """ - _check_code(code) + _check_code(code, HIP_Parameter) if isinstance(meth, str) and not hasattr(HIP, f'_read_param_{meth}'): raise RegistryError('method must be a valid HIP parameter parser function') @@ -875,11 +899,12 @@ def register_hopopt_option(code: 'IPv6_Option', meth: 'str | tuple[HOPOPT_Option It should be a subclass of :class:`pcapkit.protocols.schema.internet.hopopt.Option`. Raises: - RegistryError: If ``code`` is not an enum member, or ``meth`` + RegistryError: If ``code`` is not a :class:`~pcapkit.const.ipv6.option.Option` + member, e.g. a member of another enum, or ``meth`` is a name with no matching ``_read_opt_*`` method. """ - _check_code(code) + _check_code(code, IPv6_Option) if isinstance(meth, str) and not hasattr(HOPOPT, f'_read_opt_{meth}'): raise RegistryError('method must be a valid HOPOPT option parser function') @@ -908,11 +933,12 @@ def register_ipv6_opts_option(code: 'IPv6_Option', meth: 'str | tuple[IPv6_Opts_ It should be a subclass of :class:`pcapkit.protocols.schema.internet.ipv6_opts.Option`. Raises: - RegistryError: If ``code`` is not an enum member, or ``meth`` + RegistryError: If ``code`` is not a :class:`~pcapkit.const.ipv6.option.Option` + member, e.g. a member of another enum, or ``meth`` is a name with no matching ``_read_opt_*`` method. """ - _check_code(code) + _check_code(code, IPv6_Option) if isinstance(meth, str) and not hasattr(IPv6_Opts, f'_read_opt_{meth}'): raise RegistryError('method must be a valid IPv6-Opts option parser function') @@ -941,11 +967,12 @@ def register_ipv6_route_routing(code: 'IPv6_Routing', meth: 'str | tuple[IPv6_Ro It should be a subclass of :class:`pcapkit.protocols.schema.internet.ipv6_route.RoutingType`. Raises: - RegistryError: If ``code`` is not an enum member, or ``meth`` + RegistryError: If ``code`` is not a :class:`~pcapkit.const.ipv6.routing.Routing` + member, e.g. a member of another enum, or ``meth`` is a name with no matching ``_read_data_type_*`` method. """ - _check_code(code) + _check_code(code, IPv6_Routing) if isinstance(meth, str) and not hasattr(IPv6_Route, f'_read_data_type_{meth}'): raise RegistryError('method must be a valid IPv6-Route routing data parser function') @@ -974,11 +1001,12 @@ def register_mh_message(code: 'MH_Packet', meth: 'str | tuple[MH_PacketParser, M It should be a subclass of :class:`pcapkit.protocols.schema.internet.mh.Packet`. Raises: - RegistryError: If ``code`` is not an enum member, or ``meth`` + RegistryError: If ``code`` is not a :class:`~pcapkit.const.mh.packet.Packet` + member, e.g. a member of another enum, or ``meth`` is a name with no matching ``_read_msg_*`` method. """ - _check_code(code) + _check_code(code, MH_Packet) if isinstance(meth, str) and not hasattr(MH, f'_read_msg_{meth}'): raise RegistryError('method must be a valid MH message type parser function') @@ -1007,11 +1035,12 @@ def register_mh_option(code: 'MH_Option', meth: 'str | tuple[MH_OptionParser, MH It should be a subclass of :class:`pcapkit.protocols.schema.internet.mh.Option`. Raises: - RegistryError: If ``code`` is not an enum member, or ``meth`` + RegistryError: If ``code`` is not a :class:`~pcapkit.const.mh.option.Option` + member, e.g. a member of another enum, or ``meth`` is a name with no matching ``_read_opt_*`` method. """ - _check_code(code) + _check_code(code, MH_Option) if isinstance(meth, str) and not hasattr(MH, f'_read_opt_{meth}'): raise RegistryError('method must be a valid MH option parser function') @@ -1040,7 +1069,8 @@ def register_mh_extension(code: 'MH_CGAExtension', meth: 'str | tuple[MH_Extensi It should be a subclass of :class:`pcapkit.protocols.schema.internet.mh.CGAExtension`. Raises: - RegistryError: If ``code`` is not an enum member, or ``meth`` + RegistryError: If ``code`` is not a :class:`~pcapkit.const.mh.cga_extension.CGAExtension` + member, e.g. a member of another enum, or ``meth`` is a name with no matching ``_read_ext_*`` method. .. versionchanged:: 1.5.0 @@ -1049,7 +1079,7 @@ def register_mh_extension(code: 'MH_CGAExtension', meth: 'str | tuple[MH_Extensi that take one (:issue:`516`). """ - _check_code(code) + _check_code(code, MH_CGAExtension) if isinstance(meth, str) and not hasattr(MH, f'_read_ext_{meth}'): raise RegistryError('method must be a valid MH CGA extension function') @@ -1119,7 +1149,9 @@ def register_apptype(code: 'int | Enum_AppType', module: 'str | ModuleDescriptor :class:`int` carries no such default and so requires at least one. Raises: - pcapkit.utilities.exceptions.RegistryError: If no transport protocol is + pcapkit.utilities.exceptions.RegistryError: If ``code`` is neither an + :class:`int` nor an :class:`~pcapkit.const.reg.apptype.apptype.AppType` + member, e.g. a member of another enum; if no transport protocol is given for a bare :class:`int`; if one of those given (member or name) names no port-keyed registry; or if a ``str`` matches no :class:`~pcapkit.const.reg.apptype.apptype.TransportProtocol` member's @@ -1159,6 +1191,8 @@ def register_apptype(code: 'int | Enum_AppType', module: 'str | ModuleDescriptor protocol per argument, with no alias (:issue:`806`). """ + _check_code(code, Enum_AppType, number=True) + # NOTE: ``class_`` sits at the sibling position -- positional, ahead of # ``*transport`` -- per maintainer ruling, so a third positional that is # really a transport would be swallowed as a class name. That is @@ -1279,7 +1313,13 @@ def register_tcp(code: 'int | Enum_AppType', module: 'str | ModuleDescriptor[Pro :class:`~pcapkit.protocols.protocol.ProtocolBase` subclass class\_: class name + Raises: + RegistryError: If ``code`` is neither an :class:`int` nor an + :class:`~pcapkit.const.reg.apptype.apptype.AppType` member, e.g. a + member of another enum. + """ + _check_code(code, Enum_AppType, number=True) if isinstance(code, Enum_AppType): code = code.port if isinstance(module, str): @@ -1313,11 +1353,12 @@ def register_tcp_option(code: 'TCP_Option', meth: 'str | tuple[TCP_OptionParser, It should be a subclass of :class:`pcapkit.protocols.schema.transport.tcp.Option`. Raises: - RegistryError: If ``code`` is not an enum member, or ``meth`` + RegistryError: If ``code`` is not a :class:`~pcapkit.const.tcp.option.Option` + member, e.g. a member of another enum, or ``meth`` is a name with no matching ``_read_mode_*`` method. """ - _check_code(code) + _check_code(code, TCP_Option) if isinstance(meth, str) and not hasattr(TCP, f'_read_mode_{meth}'): raise RegistryError('method must be a TCP option parser function') @@ -1346,11 +1387,12 @@ def register_tcp_mp_option(code: 'TCP_MPTCPOption', meth: 'str | tuple[TCP_MPOpt It should be a subclass of :class:`pcapkit.protocols.schema.transport.tcp.MPTCP`. Raises: - RegistryError: If ``code`` is not an enum member, or ``meth`` + RegistryError: If ``code`` is not a :class:`~pcapkit.const.tcp.mp_tcp_option.MPTCPOption` + member, e.g. a member of another enum, or ``meth`` is a name with no matching ``_read_mptcp_*`` method. """ - _check_code(code) + _check_code(code, TCP_MPTCPOption) if isinstance(meth, str) and not hasattr(TCP, f'_read_mptcp_{meth}'): raise RegistryError('method must be a MPTCP option parser function') @@ -1384,7 +1426,13 @@ def register_udp(code: 'int | Enum_AppType', module: 'str | ModuleDescriptor[Pro :class:`~pcapkit.protocols.protocol.ProtocolBase` subclass class\_: class name + Raises: + RegistryError: If ``code`` is neither an :class:`int` nor an + :class:`~pcapkit.const.reg.apptype.apptype.AppType` member, e.g. a + member of another enum. + """ + _check_code(code, Enum_AppType, number=True) if isinstance(code, Enum_AppType): code = code.port if isinstance(module, str): @@ -1430,7 +1478,14 @@ def register_sctp(code: 'int | SCTP_PayloadProtocolIdentifier', module: 'str | M number: SCTP names its upper layer per DATA chunk rather than per association. See :rfc:`9260#section-3.3.1`. + Raises: + RegistryError: If ``code`` is neither an :class:`int` nor a + :class:`~pcapkit.const.sctp.payload_protocol_identifier.PayloadProtocolIdentifier` + member, e.g. a member of the + :class:`~pcapkit.const.reg.apptype.sctp.SCTP` port enum. + """ + _check_code(code, Enum_PayloadProtocolIdentifier, number=True) if isinstance(module, str): module = cast('ModuleDescriptor[ProtocolBase]', ModuleDescriptor(module, class_)) @@ -1467,11 +1522,12 @@ def register_http_frame(code: 'HTTP_Frame', meth: 'str | tuple[HTTP_FrameParser, It should be a subclass of :class:`pcapkit.protocols.schema.application.httpv2.FrameType`. Raises: - RegistryError: If ``code`` is not an enum member, or ``meth`` + RegistryError: If ``code`` is not a :class:`~pcapkit.const.http.frame.Frame` + member, e.g. a member of another enum, or ``meth`` is a name with no matching ``_read_http_*`` method. """ - _check_code(code) + _check_code(code, HTTP_Frame) if isinstance(meth, str) and not hasattr(HTTPv2, f'_read_http_{meth}'): raise RegistryError('method must be a frame parser function') @@ -1505,11 +1561,12 @@ def register_pcapng_block(code: 'PCAPNG_BlockType', meth: 'str | tuple[PCAPNG_Bl It should be a subclass of :class:`pcapkit.protocols.schema.misc.pcapng.BlockType`. Raises: - RegistryError: If ``code`` is not an enum member, or ``meth`` + RegistryError: If ``code`` is not a :class:`~pcapkit.const.pcapng.block_type.BlockType` + member, e.g. a member of another enum, or ``meth`` is a name with no matching ``_read_block_*`` method. """ - _check_code(code) + _check_code(code, PCAPNG_BlockType) if isinstance(meth, str) and not hasattr(PCAPNG, f'_read_block_{meth}'): raise RegistryError('method must be a block parser function') @@ -1538,11 +1595,12 @@ def register_pcapng_option(code: 'PCAPNG_OptionType', meth: 'str | tuple[PCAPNG_ It should be a subclass of :class:`pcapkit.protocols.schema.misc.pcapng.Option`. Raises: - RegistryError: If ``code`` is not an enum member, or ``meth`` + RegistryError: If ``code`` is not a :class:`~pcapkit.const.pcapng.option_type.OptionType` + member, e.g. a member of another enum, or ``meth`` is a name with no matching ``_read_option_*`` method. """ - _check_code(code) + _check_code(code, PCAPNG_OptionType) if isinstance(meth, str) and not hasattr(PCAPNG, f'_read_option_{meth}'): raise RegistryError('method must be a option parser function') @@ -1571,11 +1629,12 @@ def register_pcapng_record(code: 'PCAPNG_RecordType', meth: 'str | tuple[PCAPNG_ It should be a subclass of :class:`pcapkit.protocols.schema.misc.pcapng.NameResolutionRecord`. Raises: - RegistryError: If ``code`` is not an enum member, or ``meth`` + RegistryError: If ``code`` is not a :class:`~pcapkit.const.pcapng.record_type.RecordType` + member, e.g. a member of another enum, or ``meth`` is a name with no matching ``_read_record_*`` method. """ - _check_code(code) + _check_code(code, PCAPNG_RecordType) if isinstance(meth, str) and not hasattr(PCAPNG, f'_read_record_{meth}'): raise RegistryError('method must be a name resolution record parser function') @@ -1604,11 +1663,12 @@ def register_pcapng_secrets(code: 'PCAPNG_SecretsType', meth: 'str | tuple[PCAPN It should be a subclass of :class:`pcapkit.protocols.schema.misc.pcapng.DSBSecrets`. Raises: - RegistryError: If ``code`` is not an enum member, or ``meth`` + RegistryError: If ``code`` is not a :class:`~pcapkit.const.pcapng.secrets_type.SecretsType` + member, e.g. a member of another enum, or ``meth`` is a name with no matching ``_read_secrets_*`` method. """ - _check_code(code) + _check_code(code, PCAPNG_SecretsType) if isinstance(meth, str) and not hasattr(PCAPNG, f'_read_secrets_{meth}'): raise RegistryError('method must be a decryption secrets parser function') diff --git a/tests/foundation/registry/test_registrar_code_check_unit.py b/tests/foundation/registry/test_registrar_code_check_unit.py index 71f6f871a7..bdc9b1a631 100644 --- a/tests/foundation/registry/test_registrar_code_check_unit.py +++ b/tests/foundation/registry/test_registrar_code_check_unit.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""A registrar handed a ``code`` that is not an enum member changes nothing. C.f. #2186. +"""A registrar handed a ``code`` outside its enum changes nothing. C.f. #2186 and #2224. The registrars keyed on an enum code log ``code.name`` once the registration is done, so an ``int`` or :obj:`None` failed there with :exc:`AttributeError` after @@ -12,7 +12,16 @@ name leases -- calls the registrar with an otherwise valid argument list, and compares every registry with the snapshot. The registrars that take a plain ``int`` port or PPID (``register_tcp``, ``register_udp``, ``register_sctp`` and -``register_apptype``) are out of scope: an ``int`` is a valid code there. +``register_apptype``) are out of scope for those codes: an ``int`` is a valid +code there. + +Every registrar, those four included, also rejects a member of an enum other +than the one it is typed on (#2224). The registry enums compare equal to their +values, so such a member keys the entry of the registrar's own member of that +value: ``register_linktype(TransType.ICMP, ...)`` replaced +``Frame.__proto__[LinkType.ETHERNET]``. Each wrong-enum case picks a member +equal to one of the registrar's own where the value types allow, so the check +has to be on the type. """ @@ -81,6 +90,40 @@ #: :class:`str` (the PCAP-NG option codes are a string enum). BAD_CODES = (250, None, '250') +#: The enum each registrar above is typed on, for the wrong-enum cases (#2224). +REGISTRAR_ENUMS = { + 'register_ipv4_option': 'pcapkit.const.ipv4.option_number:OptionNumber', + 'register_hip_parameter': 'pcapkit.const.hip.parameter:Parameter', + 'register_hopopt_option': 'pcapkit.const.ipv6.option:Option', + 'register_ipv6_opts_option': 'pcapkit.const.ipv6.option:Option', + 'register_ipv6_route_routing': 'pcapkit.const.ipv6.routing:Routing', + 'register_mh_message': 'pcapkit.const.mh.packet:Packet', + 'register_mh_option': 'pcapkit.const.mh.option:Option', + 'register_mh_extension': 'pcapkit.const.mh.cga_extension:CGAExtension', + 'register_tcp_option': 'pcapkit.const.tcp.option:Option', + 'register_tcp_mp_option': 'pcapkit.const.tcp.mp_tcp_option:MPTCPOption', + 'register_http_frame': 'pcapkit.const.http.frame:Frame', + 'register_pcapng_block': 'pcapkit.const.pcapng.block_type:BlockType', + 'register_pcapng_option': 'pcapkit.const.pcapng.option_type:OptionType', + 'register_pcapng_record': 'pcapkit.const.pcapng.record_type:RecordType', + 'register_pcapng_secrets': 'pcapkit.const.pcapng.secrets_type:SecretsType', + 'register_linktype': 'pcapkit.const.reg.linktype:LinkType', + 'register_pcap': 'pcapkit.const.reg.linktype:LinkType', + 'register_pcapng': 'pcapkit.const.reg.linktype:LinkType', + 'register_ethertype': 'pcapkit.const.reg.ethertype:EtherType', + 'register_transtype': 'pcapkit.const.reg.transtype:TransType', +} + +#: Registrars that also take a plain ``int``: the port or PPID used, the classes +#: whose ``__proto__`` they write, and any trailing arguments. +NUMBER_REGISTRARS = ( + ('register_tcp', 80, ('pcapkit.protocols.transport.tcp:TCP',), ()), + ('register_udp', 53, ('pcapkit.protocols.transport.udp:UDP',), ()), + ('register_sctp', 60, ('pcapkit.protocols.transport.sctp:SCTP',), ()), + ('register_apptype', 80, ('pcapkit.protocols.transport.tcp:TCP', + 'pcapkit.protocols.transport.udp:UDP'), ('tcp',)), +) + def resolve(path: 'str') -> 'type': module, _, name = path.partition(':') @@ -105,13 +148,33 @@ def _registries(self, classes: 'tuple[type, ...]') -> 'dict[tuple[str, str], dic found[(cls.__qualname__, attr)] = value return found + def _wrong_code(self, name: 'str') -> 'object': + """A member of another enum, equal to one of ``name``'s own where the value types allow.""" + from pcapkit.const.pcapng.tls_key_label import TLSKeyLabel + from pcapkit.const.reg.linktype import LinkType + from pcapkit.const.reg.transtype import TransType + + kind = resolve(REGISTRAR_ENUMS[name]) + own = next(iter(kind)) + if kind is LinkType: + # the issue's example: ICMP == 1 == LinkType.ETHERNET + wrong, own = TransType.ICMP, LinkType.ETHERNET + elif isinstance(own.value, int): + wrong = LinkType(own.value) + else: + # the PCAP-NG option codes are a string enum, which no other enum equals + return TLSKeyLabel.RSA + self.assertNotIsInstance(wrong, kind) + self.assertEqual(wrong, own) + return wrong + def _assert_rejected_unchanged(self, name: 'str', classes: 'tuple[type, ...]', *args: 'object', - **kwargs: 'object') -> None: + codes: 'tuple[object, ...]' = BAD_CODES, **kwargs: 'object') -> None: from pcapkit.foundation.registry import protocols as registrars from pcapkit.utilities.exceptions import RegistryError registries = self._registries(classes) - for code in BAD_CODES: + for code in codes: with self.subTest(registrar=name, code=code): before = {key: dict(table) for key, table in registries.items()} try: @@ -141,6 +204,33 @@ def test_module_registrars_reject_a_non_enum_code_before_registering(self) -> No for name, targets in MODULE_REGISTRARS: self._assert_rejected_unchanged(name, tuple(map(resolve, targets)), Raw) + def test_method_registrars_reject_a_member_of_another_enum(self) -> None: + def parser(*args: 'object', **kwargs: 'object') -> None: ... + + for name, proto, schema in METHOD_REGISTRARS: + proto_cls, schema_cls = resolve(proto), resolve(schema) + self._assert_rejected_unchanged(name, (proto_cls, schema_cls), (parser, parser), + codes=(self._wrong_code(name),), schema=schema_cls) + + def test_module_registrars_reject_a_member_of_another_enum(self) -> None: + from pcapkit.protocols.misc.raw import Raw + + for name, targets in MODULE_REGISTRARS: + self._assert_rejected_unchanged(name, tuple(map(resolve, targets)), Raw, + codes=(self._wrong_code(name),)) + + def test_number_registrars_reject_a_member_of_another_enum(self) -> None: + from pcapkit.const.reg.linktype import LinkType + from pcapkit.protocols.misc.raw import Raw + + for name, number, targets, extra in NUMBER_REGISTRARS: + # an IntEnum member is an int, so accepting an int must not let it in + wrong = LinkType(number) + self.assertIsInstance(wrong, int) + self.assertEqual(wrong, number) + self._assert_rejected_unchanged(name, tuple(map(resolve, targets)), Raw, *extra, + codes=(wrong,)) + if __name__ == '__main__': unittest.main()