From 3d1dea6f11e61dd9f0a9f3f05c1e2c7e3eb2951b Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Mon, 2 Nov 2026 18:25:35 -0500 Subject: [PATCH] refactor(protocols)!: make() takes an enum member or an int, with no keyword groups (#2594) - _make_index: takes (value, enumeration, bits=, name=), checks the code as _make_uint does, and returns what get resolves; where get raises for a code the field carries, the code comes back as an int. The field width alone decides a refusal. The frame inspection and its helpers go. - Protocol.resolve_index: public helper taking the name/default/namespace/ reversed group explicitly, for a caller holding a name. - Every make() and _make_* maker, and every (code, arguments) option, sub-option, extension and chunk list: the groups are removed with no shim, each enumeration field is typed Enum | int and resolved by _make_index; a name, a wide bool or a code its field cannot carry raises ProtocolError. Also SCTP, DCCP, NGAP, IGMPv3, IPv6_Ext and the transport ports, and MH Binding Error's status, typed BindingError. - Breaking: an int HTTP/1 status with no message writes its member's phrase, as a member did: 404 writes "404 Not Found" where it wrote "404 ", and a code the registry does not assign writes its minted member's phrase ("299 Unassigned", "600 Unknown"). - Docs: versionchanged/versionremoved on every class that shipped the keywords; pep.rst and registry-protocol.rst record the rule. - Tests: a guard against the groups and frame inspection, refusal tables per family and a PCAP dumper test that fail on main; name-inference tests rewritten. Every tests/ leg matches main's results. Closes #2693 --- .../source/contributing/conventions/index.rst | 3 +- .../conventions/registry-protocol.rst | 37 + docs/source/contributing/pep.rst | 126 ++- docs/source/pcapkit/protocols/protocol.rst | 7 +- examples/generators/dispatch.py | 4 +- .../generators/dispatch_families/linktype.py | 4 +- .../generators/dispatch_families/radiotap.py | 4 +- pcapkit/dumpkit/pcap.py | 6 +- pcapkit/protocols/application/dtls.py | 30 +- pcapkit/protocols/application/httpv1.py | 81 +- pcapkit/protocols/application/httpv2.py | 79 +- pcapkit/protocols/application/ngap.py | 30 +- pcapkit/protocols/application/ospf.py | 55 +- pcapkit/protocols/application/ssl.py | 7 +- pcapkit/protocols/application/sslv2.py | 57 +- pcapkit/protocols/application/tls.py | 27 +- pcapkit/protocols/internet/ah.py | 37 +- pcapkit/protocols/internet/esp.py | 38 +- pcapkit/protocols/internet/hip.py | 643 +++++----------- pcapkit/protocols/internet/hopopt.py | 160 ++-- pcapkit/protocols/internet/icmpv4.py | 23 +- pcapkit/protocols/internet/icmpv6.py | 23 +- pcapkit/protocols/internet/igmp.py | 24 +- pcapkit/protocols/internet/igmpv3.py | 34 +- pcapkit/protocols/internet/ipv4.py | 188 ++--- pcapkit/protocols/internet/ipv6.py | 44 +- pcapkit/protocols/internet/ipv6_ext.py | 21 +- pcapkit/protocols/internet/ipv6_frag.py | 37 +- pcapkit/protocols/internet/ipv6_opts.py | 160 ++-- pcapkit/protocols/internet/ipv6_route.py | 66 +- pcapkit/protocols/internet/ipx.py | 35 +- pcapkit/protocols/internet/mh.py | 722 ++++++++---------- pcapkit/protocols/link/arp.py | 63 +- pcapkit/protocols/link/ethernet.py | 41 +- pcapkit/protocols/link/gre.py | 28 +- pcapkit/protocols/link/ieee802_11.py | 38 +- pcapkit/protocols/link/l2tpv2.py | 39 +- pcapkit/protocols/link/linux_sll1.py | 8 +- pcapkit/protocols/link/linux_sll2.py | 8 +- pcapkit/protocols/link/vlan.py | 57 +- pcapkit/protocols/misc/checksum/ip_pseudo.py | 24 +- pcapkit/protocols/misc/pcap/header.py | 39 +- pcapkit/protocols/misc/pcapng.py | 280 +++---- pcapkit/protocols/protocol.py | 322 +++----- pcapkit/protocols/transport/dccp.py | 57 +- pcapkit/protocols/transport/sctp.py | 94 ++- pcapkit/protocols/transport/tcp.py | 83 +- pcapkit/protocols/transport/transport.py | 20 +- .../test_pcapio_linktype_name_2594_unit.py | 76 ++ .../test_make_keyword_groups_2594_unit.py | 188 +++++ .../test_rfc_constants_one_home_1739_unit.py | 9 +- .../test_application_make_codes_2594_unit.py | 217 ++++++ .../test_ospf_checksum_2475_unit.py | 8 +- .../test_tls_extensions_2514_unit.py | 3 +- ...t_hip_list_argument_str_names_2394_unit.py | 225 +++--- .../internet/test_hip_make_codes_2594_unit.py | 388 ++++++++++ tests/protocols/internet/test_hip_unit.py | 16 +- .../internet/test_hip_wire_semantics_unit.py | 22 +- .../test_icmp_base_dispatch_2574_unit.py | 3 +- .../internet/test_icmp_checksum_2470_unit.py | 27 +- .../internet/test_igmp_checksum_2618_unit.py | 4 +- tests/protocols/internet/test_igmp_unit.py | 5 +- .../test_internet_maker_ranges_2614_unit.py | 29 +- .../internet/test_ip_make_codes_2594_unit.py | 246 ++++++ .../internet/test_ipv4_checksum_2472_unit.py | 4 +- .../test_ipv4_later_fragment_raw_unit.py | 6 +- ...test_ipv4_tso_total_length_1547_runtime.py | 8 +- .../test_ipv6_ext_make_codes_2594_unit.py | 232 ++++++ .../internet/test_mh_make_codes_2594_unit.py | 461 +++++++++++ tests/protocols/internet/test_mh_unit.py | 88 ++- .../test_ieee802_11_make_codes_2594_unit.py | 80 ++ tests/protocols/link/test_ieee802_11_unit.py | 7 +- .../link/test_link_make_codes_2594_unit.py | 171 +++++ .../misc/checksum/test_ip_pseudo_2471_unit.py | 17 +- ...t_capture_message_prefix_2300_2301_unit.py | 65 +- .../misc/test_misc_make_codes_2594_unit.py | 284 +++++++ tests/protocols/test_make_index_str_unit.py | 327 ++++---- tests/protocols/test_protocol_base_unit.py | 25 +- tests/protocols/test_protocol_make_unit.py | 86 --- tests/protocols/transport/test_dccp_unit.py | 14 +- .../test_sctp_make_codes_2594_unit.py | 281 +++++++ .../transport/test_tcp_checksum_2473_unit.py | 14 +- .../test_transport_make_codes_2594_unit.py | 217 ++++++ .../transport/test_udp_checksum_2474_unit.py | 18 +- 84 files changed, 5174 insertions(+), 2710 deletions(-) create mode 100644 tests/dumpkit/test_pcapio_linktype_name_2594_unit.py create mode 100644 tests/project/test_make_keyword_groups_2594_unit.py create mode 100644 tests/protocols/application/test_application_make_codes_2594_unit.py create mode 100644 tests/protocols/internet/test_hip_make_codes_2594_unit.py create mode 100644 tests/protocols/internet/test_ip_make_codes_2594_unit.py create mode 100644 tests/protocols/internet/test_ipv6_ext_make_codes_2594_unit.py create mode 100644 tests/protocols/internet/test_mh_make_codes_2594_unit.py create mode 100644 tests/protocols/link/test_ieee802_11_make_codes_2594_unit.py create mode 100644 tests/protocols/link/test_link_make_codes_2594_unit.py create mode 100644 tests/protocols/misc/test_misc_make_codes_2594_unit.py create mode 100644 tests/protocols/transport/test_sctp_make_codes_2594_unit.py create mode 100644 tests/protocols/transport/test_transport_make_codes_2594_unit.py diff --git a/docs/source/contributing/conventions/index.rst b/docs/source/contributing/conventions/index.rst index e5f808118f..2894082966 100644 --- a/docs/source/contributing/conventions/index.rst +++ b/docs/source/contributing/conventions/index.rst @@ -14,7 +14,8 @@ House Conventions questions have mostly arisen; * :ref:`sentinel-convention` -- :mod:`pcapkit.corekit`; * :ref:`registry-protocol` -- :mod:`pcapkit.const` again, the registration - wrappers of :mod:`pcapkit.foundation.registry`, and the value a protocol's + wrappers of :mod:`pcapkit.foundation.registry`, how a ``make()`` field holding + an enumeration resolves, 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 diff --git a/docs/source/contributing/conventions/registry-protocol.rst b/docs/source/contributing/conventions/registry-protocol.rst index 664a10bdba..d732f84121 100644 --- a/docs/source/contributing/conventions/registry-protocol.rst +++ b/docs/source/contributing/conventions/registry-protocol.rst @@ -878,6 +878,43 @@ above, and each type's layout is a :class:`~pcapkit.protocols.schema.link.radiotap.RadiotapField` subclass, whose fields are the type's subfields and whose ``__align__`` is its alignment. +.. _registry-protocol-make-fields: + +Enumeration Fields of ``make()`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**A** ``make()`` **field that holds an enumeration takes the member or an** +:class:`int` **code, resolved with the enumeration's** +:meth:`~pcapkit.corekit.enum.EnumLookup.get`. Ruled in review of :pr:`2593` +(:issue:`2489`), for every ``make()`` and every ``_make_*`` maker. Nothing inside +``make()`` turns a :obj:`str` into a member: a name is refused, and a caller holding +one resolves it first with :meth:`Protocol.resolve_index +`, which takes the name, a +default, the namespace and whether a :obj:`dict` namespace maps names to codes. No +such field takes an ``X_default``, ``X_namespace`` or ``X_reversed`` keyword beside +it, and none is kept as a shim, ruled on :issue:`2594`. + +* **Resolve through** :meth:`~pcapkit.protocols.protocol.Protocol._make_index`, giving + the enumeration and the field's width in bits. It checks the code as + ``_make_uint()`` checks an integer -- a :obj:`bool` passes only for a one-bit + field, and a negative or too wide code is refused -- and each refusal is + :exc:`~pcapkit.utilities.exceptions.ProtocolError`, never a masked or wrapped + code, by :issue:`2617`'s ruling. The field's width decides what is refused, not + the enumeration's range, so every code the field carries builds and parses + back. It returns what ``get`` resolves, which is a minted ``Unassigned`` member + for most registries' unassigned codes. Where ``get`` raises :exc:`ValueError` for + a code the field carries, it returns the code itself as an :class:`int`. +* **An option, a sub-option or a chunk given as a** ``(code, arguments)`` **pair + resolves its code the same way**, before anything reads it, so that a + :obj:`bool` is refused rather than read as a padding code. +* **The enumeration is named at the call**, never read from the caller's frame or + from its arguments' defaults: that inference is what :issue:`2356` and + :issue:`2394` broke. + +:file:`tests/project/test_make_keyword_groups_2594_unit.py` fails on a maker taking +an ``X_default``, ``X_namespace`` or ``X_reversed`` parameter, on a ``_make_index`` +call without the field's width, and on the resolver reaching a frame. + .. _registry-protocol-index: The Registry Index diff --git a/docs/source/contributing/pep.rst b/docs/source/contributing/pep.rst index d21d6c573e..b7c595e61b 100644 --- a/docs/source/contributing/pep.rst +++ b/docs/source/contributing/pep.rst @@ -991,70 +991,59 @@ since ESP has already laid the groundwork. Deterministic Construction Arguments ------------------------------------ -This is meant to open a broader run of proposals for the construction -(``make()``) workflow in general rather than stand alone -- more will be added -under that heading as they get scoped, and the concrete item ready to write -down now is making a ``make()`` call's own argument list deterministic. - -:meth:`~pcapkit.protocols.protocol.Protocol._make_index` is where that is not -currently true. A number of ``make()`` methods accept a plain ``str`` -enum-member name for a field and resolve it by frame-introspecting the -caller's locals for a conventional four-argument group -- ``X``, -``X_default``, ``X_namespace`` and ``X_reversed`` -- looking for the one -argument whose own default is an enum member, and taking that member's class -as the namespace to search. The actual contract of a ``make()`` call is -therefore not fully visible from its own signature: knowing what a bare -string resolves against means knowing the convention and trusting that every -call site followed it exactly, rather than reading it off the call itself. -That gap is exactly how this round's bug family happened. :issue:`2356` is -the walk coming up empty because the relevant argument's own default is -``None``, leaving nothing to anchor a namespace to; :issue:`2394` is the same -inference failing by a different route -- the name is a loop variable over a -*list* parameter rather than a scalar argument, so the walk's -``isinstance(value, str)`` check can never see it. Both were fixed narrowly, -by naming the correct enum explicitly at each affected call site rather than -touching the inference, so the mechanism is exactly as fragile as it was -before and just as liable to the same shape of defect at the next call site -that does not fit its assumptions precisely. - -The owner's proposed direction -- recorded here, and explicitly **not** -scheduled for 1.5.0 -- removes the inference rather than patching it again. -``make()`` would require an actual enum value, or an ``int`` where one is -already accepted, for these fields, with no implicit string-to-enum -resolution performed inside ``make()`` itself. In its place, a small public -wrapper would take the same four-argument group explicitly -- name, default, -namespace, reversed -- and *resolve* a string name to its enum member on -request, so a caller holding a bare string calls that helper first and passes -the resolved value into ``make()``. That mirrors how a finalised -:class:`~pcapkit.corekit.infoclass.Info` or -:class:`~pcapkit.protocols.schema.schema.Schema` already settles its -constructor's keyword set once, when -:func:`~pcapkit.corekit.infoclass.info_final` or -:func:`~pcapkit.protocols.schema.schema.schema_final` is applied at -class-definition time, rather than inferring it afresh at every call -- +This opens a broader run of proposals for the construction (``make()``) +workflow in general -- more go under this heading as they get scoped -- and +its first item, making a ``make()`` call's own argument list deterministic, is +**done for 1.6.0** (:issue:`2594`). + +Inferring a namespace from the caller's frame, for a conventional +four-argument group -- ``X``, ``X_default``, ``X_namespace`` and +``X_reversed`` -- leaves a call's contract invisible from its signature: +knowing what a bare string resolves against means knowing the convention and +trusting that every call site follows it exactly. :issue:`2356` and +:issue:`2394` are that inference failing, the first because an argument's own +default is ``None``, leaving nothing to anchor a namespace to, the second +because the name is a loop variable over a *list* parameter, which an +``isinstance(value, str)`` check on the arguments cannot see. Naming the enum +at each affected call site fixes an instance and leaves the mechanism as +fragile as it is. + +So no ``make()`` infers a member from a string, ruled in review of :pr:`2593` +(:issue:`2489`): a field holding an enumeration takes the member or an +``int``, resolved with the enumeration's +:meth:`~pcapkit.corekit.enum.EnumLookup.get`. That settles a call's contract +at the signature, the way :func:`~pcapkit.corekit.infoclass.info_final` and +:func:`~pcapkit.protocols.schema.schema.schema_final` settle a constructor's +keyword set once at class-definition time, and :attr:`~pcapkit.protocols.protocol.Protocol.__own_make_keywords__` -(:issue:`2286`) settles the same kind of question explicitly rather than -inferring it, though for a different question: which keywords a ``make()`` -override owns, not what the names passed to it resolve against. - -The trade-off is real and worth stating rather than glossed over. Accepting a -bare string for these fields is a pattern used throughout the docs and the -examples, so dropping the inference is a likely-breaking change at every -``make()`` call site that currently relies on it -- not a one-line fix. -Deciding that scope -- which call sites change, whether string acceptance is -dropped outright or only the silent-inference path behind it, and what the -wrapper is named and where it lives -- is itself design work, which is -exactly why this belongs on this page rather than as a quick issue. Done, it -removes the root cause of the :issue:`2356`/:issue:`2394` family rather than -leaving its next instance to be found and patched the same way. - -**Scoped for 1.6.0.** The owner took this direction for every ``make()``, ruled -in review of :pr:`2593` (:issue:`2489`): a field holding an enum takes the member -or an ``int``, resolved with the enumeration's ``get`` rather than inferred from -the caller's frame. The shipped ``X_default``, ``X_namespace`` and ``X_reversed`` -keywords go in 1.6.0 with no deprecation period, ruled on :issue:`2594`, which -carries the change across :file:`pcapkit/protocols/` in one pass and leaves the -helper's name and module to it. +(:issue:`2286`) which keywords a ``make()`` override owns. The pieces: + +* :meth:`~pcapkit.protocols.protocol.Protocol._make_index` takes the value, the + enumeration and the field's width, and returns what ``get`` resolves. A + name, a :obj:`bool` for a field wider than one bit, and a code the field + cannot carry raise :exc:`~pcapkit.utilities.exceptions.ProtocolError`, so + every caller gets the range check :issue:`2617` asks for, with no masking. + An option, a sub-option or a chunk given as a ``(code, arguments)`` pair + resolves its code the same way. +* The ``X_default``, ``X_namespace`` and ``X_reversed`` keywords are removed + in 1.6.0 with no deprecation period and no shim, ruled on :issue:`2594`; + each class that shipped them records them under ``versionremoved``. +* The public wrapper is :meth:`Protocol.resolve_index + `, which takes the group + explicitly -- name, default, namespace, reversed -- so a caller holding a + bare string resolves it first and passes the result to ``make()``. It is a + static method every protocol inherits, beside + :meth:`~pcapkit.protocols.protocol.Protocol.decode` and + :meth:`~pcapkit.protocols.protocol.Protocol.unquote`, rather than a module of + its own, since it serves ``make()`` alone. + +The trade-off: a bare string for these fields is a pattern the docs and the +examples relied on, so the change breaks every ``make()`` call site that +relies on the inference. That is why it is one change across +:file:`pcapkit/protocols/` rather than folded into the next defect fix on this +family, and it removes the root cause of the :issue:`2356`/:issue:`2394` +family rather than leaving its next instance to be found and patched the same +way. Release Plan — 1.5.0 in Two Steps --------------------------------- @@ -1387,10 +1376,9 @@ as are IGMP's and GRE's (:issue:`2618`). the parsing and the constructing path, rather than a back-reference from a parsed protocol to its parent. -**Scoped for 1.6.0 — deterministic construction arguments** (:issue:`2594`). See +**Deterministic construction arguments — done** (:issue:`2594`). See `Deterministic Construction Arguments`_ above: every ``make()`` takes an enum -field as the member or an ``int``, dropping the implicit string-to-enum inference -and the ``X_default``, ``X_namespace`` and ``X_reversed`` keywords behind it, and -an explicit helper resolves a bare string. That breaks every call site relying on -the inference, so it lands as one change rather than folded into the next defect -fix on this family. +field as the member or an ``int``, with no string-to-enum inference and no +``X_default``, ``X_namespace`` and ``X_reversed`` keywords behind it, and +:meth:`Protocol.resolve_index ` +resolves a bare string first. diff --git a/docs/source/pcapkit/protocols/protocol.rst b/docs/source/pcapkit/protocols/protocol.rst index 31624f7dca..5c4466b25f 100644 --- a/docs/source/pcapkit/protocols/protocol.rst +++ b/docs/source/pcapkit/protocols/protocol.rst @@ -58,6 +58,7 @@ Internal Definitions below. .. automethod:: decode .. automethod:: unquote + .. automethod:: resolve_index .. automethod:: expand_comp @@ -194,6 +195,7 @@ Internal Definitions pack decode unquote + resolve_index expand_comp _read_packet _get_payload @@ -225,8 +227,9 @@ Internal Definitions :meth:`~Protocol.from_data`, :meth:`~Protocol.read`, :meth:`~Protocol.make`, :meth:`~Protocol.unpack`, :meth:`~Protocol.pack`, :meth:`~Protocol.decode`, :meth:`~Protocol.unquote`, - :meth:`~Protocol.expand_comp`, :meth:`~Protocol._read_packet`, - :meth:`~Protocol._get_payload`, :meth:`~Protocol._make_data`, + :meth:`~Protocol.resolve_index`, :meth:`~Protocol.expand_comp`, + :meth:`~Protocol._read_packet`, :meth:`~Protocol._get_payload`, + :meth:`~Protocol._make_data`, :meth:`~Protocol._make_index`, :meth:`~Protocol._make_payload`, :meth:`~Protocol._packing_target`, :meth:`~Protocol._read_mac_addr`, :meth:`~Protocol._make_mac_addr`, diff --git a/examples/generators/dispatch.py b/examples/generators/dispatch.py index 79264142c2..591b326e6c 100644 --- a/examples/generators/dispatch.py +++ b/examples/generators/dispatch.py @@ -325,9 +325,11 @@ def _linux_sll_payload(arphrd_type: 'Any') -> 'tuple[int, bytes]': from pcapkit.protocols.link.gre import GRE return 0x0800, bytes(GRE(protocol_type=0x0800, payload=_ip4(6, _tcp(9999)))) if arphrd_type == ARPHRD.IEEE80211_RADIOTAP: + from pcapkit.const.ieee802_11.control_subtype import ControlSubtype + from pcapkit.const.ieee802_11.type import Type from pcapkit.protocols.link.ieee802_11 import IEEE802_11 from pcapkit.protocols.link.radiotap import Radiotap - ack = bytes(IEEE802_11(type='Control', subtype='ACK', addr1='02:00:00:00:00:01')) + ack = bytes(IEEE802_11(type=Type.Control, subtype=ControlSubtype.ACK, addr1='02:00:00:00:00:01')) return 0, bytes(Radiotap(payload=ack)) if arphrd_type == ARPHRD.NETLINK: from pcapkit.protocols.link.netlink import Netlink diff --git a/examples/generators/dispatch_families/linktype.py b/examples/generators/dispatch_families/linktype.py index 6a55899495..743cb4bdca 100644 --- a/examples/generators/dispatch_families/linktype.py +++ b/examples/generators/dispatch_families/linktype.py @@ -67,8 +67,10 @@ def _payload(code: 'Any') -> 'tuple[bytes, bool]': def _wlan_ack() -> 'bytes': """An 802.11 Ack frame, which carries no body.""" + from pcapkit.const.ieee802_11.control_subtype import ControlSubtype + from pcapkit.const.ieee802_11.type import Type from pcapkit.protocols.link.ieee802_11 import IEEE802_11 - return bytes(IEEE802_11(type='Control', subtype='ACK', addr1='02:00:00:00:00:01')) + return bytes(IEEE802_11(type=Type.Control, subtype=ControlSubtype.ACK, addr1='02:00:00:00:00:01')) def _radiotap(payload: 'bytes') -> 'bytes': diff --git a/examples/generators/dispatch_families/radiotap.py b/examples/generators/dispatch_families/radiotap.py index 2cdfbdc752..abb6d269a0 100644 --- a/examples/generators/dispatch_families/radiotap.py +++ b/examples/generators/dispatch_families/radiotap.py @@ -30,6 +30,8 @@ def _build(code: 'Any') -> 'Any': case. """ + from pcapkit.const.ieee802_11.control_subtype import ControlSubtype + from pcapkit.const.ieee802_11.type import Type from pcapkit.const.linux.arphrd import ARPHRD from pcapkit.const.reg.linktype import LinkType from pcapkit.protocols.link.ieee802_11 import IEEE802_11 @@ -38,7 +40,7 @@ def _build(code: 'Any') -> 'Any': if code != LinkType.IEEE802_11: raise ValueError(f'no radiotap case for link type {code!r}') - ack = bytes(IEEE802_11(type='Control', subtype='ACK', addr1='02:00:00:00:00:01')) + ack = bytes(IEEE802_11(type=Type.Control, subtype=ControlSubtype.ACK, addr1='02:00:00:00:00:01')) frame = bytes(LinuxSLL1(arphrd_type=ARPHRD.IEEE80211_RADIOTAP, payload=bytes(Radiotap(payload=ack)))) return generator._extract_first(generator._pcap_bytes((frame,), linktype=int(LinkType.LINUX_SLL)), '.pcap') diff --git a/pcapkit/dumpkit/pcap.py b/pcapkit/dumpkit/pcap.py index 56acfa3185..40f9c70d39 100644 --- a/pcapkit/dumpkit/pcap.py +++ b/pcapkit/dumpkit/pcap.py @@ -14,6 +14,7 @@ import sys from typing import TYPE_CHECKING +from pcapkit.const.reg.linktype import LinkType as Enum_LinkType from pcapkit.dumpkit.common import DumperBase from pcapkit.protocols.data.misc.pcap.header import Header as Data_Header from pcapkit.protocols.misc.pcap.header import Header @@ -26,7 +27,6 @@ from aenum import IntEnum as AenumIntEnum from typing_extensions import Literal - from pcapkit.const.reg.linktype import LinkType as Enum_LinkType from pcapkit.corekit.version import VersionInfo from pcapkit.protocols.data.misc.pcap.frame import Frame as Data_Frame from pcapkit.protocols.data.misc.pcap.frame import FrameInfo as Data_FrameInfo @@ -178,7 +178,9 @@ def _dump_header(self, *, protocol: 'Enum_LinkType | StdlibIntEnum | AenumIntEnu """ header = Header( - network=protocol, + # NOTE: Header.make takes a member or a code (#2594); this method's + # protocol also takes a name, so it is resolved here. + network=Header.resolve_index(protocol, namespace=Enum_LinkType), byteorder=byteorder, nanosecond=nanosecond, version=version, diff --git a/pcapkit/protocols/application/dtls.py b/pcapkit/protocols/application/dtls.py index 8f3e6ecad2..3a5347cb88 100644 --- a/pcapkit/protocols/application/dtls.py +++ b/pcapkit/protocols/application/dtls.py @@ -122,10 +122,8 @@ from pcapkit.utilities.exceptions import CaughtByCaller, ProtocolError if TYPE_CHECKING: - from enum import IntEnum as StdlibEnum - from typing import Any, Iterable, Optional, Type + from typing import Any, Iterable, Optional - from aenum import IntEnum as AenumEnum from typing_extensions import Literal from pcapkit.const.tls.handshake_type import HandshakeType as Enum_HandshakeType @@ -501,10 +499,7 @@ def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_DTLS': return self._read_dtls_record(length, **kwargs) def make(self, - content_type: 'Enum_ContentType | StdlibEnum | AenumEnum | str | int' = Enum_ContentType.application_data, # pylint: disable=line-too-long - content_type_default: 'Optional[int]' = None, - content_type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - content_type_reversed: 'bool' = False, + content_type: 'Enum_ContentType | int' = Enum_ContentType.application_data, version: 'int' = 0xFEFD, epoch: 'int' = 0, sequence_number: 'int' = 0, @@ -520,9 +515,6 @@ def make(self, Args: content_type: Record content type. - content_type_default: Default value for ``content_type``. - content_type_namespace: Namespace for ``content_type``. - content_type_reversed: Reverse namespace for ``content_type``. version: Legacy record version (``major << 8 | minor``), e.g. ``0xFEFD`` for DTLS 1.2 and (the legacy value DTLS 1.3 keeps here) DTLS 1.3. @@ -563,9 +555,13 @@ def make(self, Raises: ProtocolError: If ``cid`` is invalid, missing for a ``tls12_cid`` - record, or given for a legacy-format one of another type. - StructError: If a record-header field, or a field of ``content``, - does not fit its width. + record, or given for a legacy-format one of another type; if a + legacy-format record's ``content_type`` is a name, a + :obj:`bool`, or a member or code the 8-bit content type field + cannot carry; or if an extension of ``content`` given as a + ``(type, kwargs)`` pair has such a type for its 16-bit field. + StructError: If another record-header field, or a field of + ``content``, does not fit its width. FieldValueError: If a unified-header record's ``epoch`` does not fit its two bits. @@ -594,17 +590,15 @@ def make(self, payload=payload, ) - content_type_ = cast('Enum_ContentType', self._make_index( - content_type, content_type_default, namespace=content_type_namespace, - reversed=content_type_reversed, pack=False)) + content_type_ = self._make_index(content_type, Enum_ContentType, bits=8, name='content type') fragment = data if data is not None else self._make_tls_content(content_type_, content, dtls=True) # NOTE: the schema masks a field to its width, so each is packed here first, to # refuse a wider value with the StructError the content makers raise. - for value, size in ((int(content_type_), 1), (version, 2), (epoch, 2), (len(fragment), 2)): + for value, size in ((version, 2), (epoch, 2), (len(fragment), 2)): self._make_pack(value, size=size) self._make_pack(sequence_number, size=6) - if int(content_type_) == Enum_ContentType.tls12_cid: + if content_type_ == Enum_ContentType.tls12_cid: if cid is None or not _is_cid(cid): raise ProtocolError(f'{self.alias}: invalid connection ID: {cid!r} ' '(a tls12_cid record carries 1 to 255 octets)') diff --git a/pcapkit/protocols/application/httpv1.py b/pcapkit/protocols/application/httpv1.py index 6c75f022b3..25ca29c663 100644 --- a/pcapkit/protocols/application/httpv1.py +++ b/pcapkit/protocols/application/httpv1.py @@ -60,11 +60,8 @@ class is renamed :class:`~pcapkit.protocols.application.httpv1.HTTPv1`, from pcapkit.utilities.exceptions import ProtocolError if TYPE_CHECKING: - from enum import IntEnum as StdlibEnum from typing import Any, Optional, Sequence - from typing import Type as _Type - from aenum import IntEnum as AenumEnum from typing_extensions import Literal from pcapkit.protocols.data.application.httpv1 import Header as Data_Header @@ -94,7 +91,27 @@ class Type(EnumLookup, StrEnum): class HTTPv1(HTTPBase[Data_HTTP, Schema_HTTP], data=Data_HTTP, schema=Schema_HTTP): - """This class implements Hypertext Transfer Protocol (HTTP/1.*).""" + """This class implements Hypertext Transfer Protocol (HTTP/1.*). + + .. versionchanged:: 1.6.0 + + ``status`` of :meth:`make` takes a + :class:`~pcapkit.const.http.status_code.StatusCode` member or an + :class:`int` code, resolved with its ``get``, so a code given with no + ``message`` writes its member's message, as the member does; a name, a + :obj:`bool`, or a code past ``999`` raises + :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than + resolving against the enumeration or writing more than three digits + (:issue:`2594`). + + .. versionremoved:: 1.6.0 + + The ``status_default``, ``status_namespace`` and ``status_reversed`` + arguments of :meth:`make`; resolve a name with + :meth:`~pcapkit.protocols.protocol.Protocol.resolve_index` first + (:issue:`2594`). + + """ ########################################################################## # Defaults. @@ -184,10 +201,7 @@ def make(self, # type: ignore[override] http_version: 'Literal["0.9", "1.0", "1.1", b"0.9", b"1.0", b"1.1"]' = '1.1', method: 'Optional[Enum_Method | str | bytes]' = None, uri: 'Optional[str | bytes]' = None, - status: 'Optional[Enum_StatusCode | str | bytes | int]' = None, - status_default: 'Optional[int]' = None, - status_namespace: 'Optional[dict[str, int] | dict[int, str] | _Type[StdlibEnum] | _Type[AenumEnum]]' = None, # pylint: disable=line-too-long - status_reversed: 'bool' = False, + status: 'Optional[Enum_StatusCode | int]' = None, message: 'Optional[str | bytes]' = None, headers: 'Optional[OrderedMultiDict[str, str]]' = None, body: 'bytes' = b'', @@ -212,10 +226,9 @@ def make(self, # type: ignore[override] method: HTTP method. uri: HTTP request URI. status: HTTP status code. - status_default: Default HTTP status code. - status_namespace: Namespace of HTTP status code. - status_reversed: Whether to reverse the namespace. - message: HTTP status message. + message: HTTP status message; if not given, the message + :class:`~pcapkit.const.http.status_code.StatusCode` gives + ``status``. headers: HTTP headers. body: HTTP body. charset: Encoding for a :obj:`str` request URI or status message, @@ -233,13 +246,12 @@ def make(self, # type: ignore[override] response's, if they are a request's and ``uri`` is :obj:`None`, or if ``raw_line`` or a ``raw_header`` entry that reads back as the given values carries a CR or LF that the - values do not (other than the CRLF of an ``obs-fold``). + values do not (other than the CRLF of an ``obs-fold``), or if + ``status`` is a name, a :obj:`bool`, or a member or code that + is not three decimal digits. """ header_line = self._make_start_line(http_version, method=method, uri=uri, status=status, - status_default=status_default, - status_namespace=status_namespace, - status_reversed=status_reversed, message=message, charset=charset) if raw_line is not None: try: @@ -507,10 +519,7 @@ def _read_start_line(self, startline: 'bytes') -> 'Data_Header': def _make_start_line(self, http_version: 'str | bytes', *, method: 'Optional[Enum_Method | str | bytes]' = None, uri: 'Optional[str | bytes]' = None, - status: 'Optional[Enum_StatusCode | str | bytes | int]' = None, - status_default: 'Optional[int]' = None, - status_namespace: 'Optional[dict[str, int] | dict[int, str] | _Type[StdlibEnum] | _Type[AenumEnum]]' = None, # pylint: disable=line-too-long - status_reversed: 'bool' = False, + status: 'Optional[Enum_StatusCode | int]' = None, message: 'Optional[str | bytes]' = None, charset: 'Optional[str]' = None) -> 'bytes': """Make an HTTP/1.* start line from its values. @@ -520,10 +529,9 @@ def _make_start_line(self, http_version: 'str | bytes', *, method: HTTP method. uri: HTTP request URI. status: HTTP status code. - status_default: Default HTTP status code. - status_namespace: Namespace of HTTP status code. - status_reversed: Whether to reverse the namespace. - message: HTTP status message. + message: HTTP status message; if not given, the message + :class:`~pcapkit.const.http.status_code.StatusCode` gives + ``status``. charset: Encoding for a :obj:`str` request URI or status message, **UTF-8** if not given. @@ -532,8 +540,9 @@ def _make_start_line(self, http_version: 'str | bytes', *, Raises: ProtocolError: If the values are neither a request's nor a - response's, or if they are a request's and ``uri`` is - :obj:`None`. + response's, if they are a request's and ``uri`` is + :obj:`None`, or if ``status`` is a name, a :obj:`bool`, or a + member or code that is not three decimal digits. """ version = http_version.encode() if isinstance(http_version, str) else http_version @@ -553,24 +562,20 @@ def _make_start_line(self, http_version: 'str | bytes', *, header_line = b'%s %s HTTP/%s\r\n' % (meth, uri_val, version) elif method is None and status is not None: - # NOTE: ``status`` defaults to :data:`None`, so ``_make_index`` has no - # enum default to infer the namespace of a :obj:`str` member name from; - # name it here rather than leave the name unresolvable (#2356). - status_code = self._make_index( - status, status_default, - namespace=status_namespace if status_namespace is not None else Enum_StatusCode, - reversed=status_reversed, pack=False) - status_code_val = int(status_code) + # NOTE: 10 bits cover the three decimal digits of a status code (RFC + # 9112, section 4); 1000 to 1023 are past them, so refused here. + status_code = self._make_index(status, Enum_StatusCode, bits=10, name='status code', + alias='HTTP') + if int(status_code) > 999: + raise ProtocolError(f'HTTP: invalid status code: {status!r}') if message is None: - msg = getattr(status, 'message', None) or getattr(status_code, 'message', b'') or b'' + msg = getattr(status_code, 'message', '').encode() else: msg = message.encode(charset or 'utf-8') if isinstance(message, str) else message - if isinstance(msg, str): - msg = msg.encode() # status-code is 3DIGIT (RFC 9112, section 4), so keep a leading zero - header_line = b'HTTP/%s %03d %s\r\n' % (version, status_code_val, msg) + header_line = b'HTTP/%s %03d %s\r\n' % (version, int(status_code), msg) else: raise ProtocolError('HTTP: invalid start line: must have either a method or a status, ' f'not method={method!r} and status={status!r}') diff --git a/pcapkit/protocols/application/httpv2.py b/pcapkit/protocols/application/httpv2.py index f0aa8242c1..3c875827af 100644 --- a/pcapkit/protocols/application/httpv2.py +++ b/pcapkit/protocols/application/httpv2.py @@ -113,10 +113,8 @@ class is renamed :class:`~pcapkit.protocols.application.httpv2.HTTPv2`, from pcapkit.utilities.warnings import ProtocolWarning, RegistryWarning, warn if TYPE_CHECKING: - from enum import IntEnum as StdlibEnum - from typing import Any, Callable, DefaultDict, Optional, Tuple, Type + from typing import Any, Callable, DefaultDict, Optional, Tuple - from aenum import IntEnum as AenumEnum from mypy_extensions import DefaultArg, KwArg, NamedArg from typing_extensions import Literal @@ -212,6 +210,22 @@ class HTTPv2(HTTPBase[Data_HTTP, Schema_HTTP], settings past the data read as zeros, as a cut ``DATA`` frame's data is, where it was :class:`~pcapkit.protocols.misc.raw.Raw` (:issue:`2659`, :issue:`2686`). + * Each enumeration field of :meth:`make` and of the frame makers takes + a member or an :class:`int` code, resolved with the enumeration's + ``get``; a name, a :obj:`bool` for a field wider than one bit, or a + code the field cannot carry raises + :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than + resolving against an enumeration inferred from the caller's frame or + writing a different value (:issue:`2594`). + + .. versionremoved:: 1.6.0 + + The ``type_default``, ``type_namespace`` and ``type_reversed`` + arguments of :meth:`make`, and the ``error_default``, + ``error_namespace`` and ``error_reversed`` arguments of the + ``RST_STREAM`` and ``GOAWAY`` frame makers; resolve a name with + :meth:`~pcapkit.protocols.protocol.Protocol.resolve_index` first + (:issue:`2594`). """ @@ -412,10 +426,7 @@ def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_HTTP': return self._decode_next_layer(http, -1, length - 9 - schema.length) def make(self, # type: ignore[override] - type: 'Enum_Frame | StdlibEnum | AenumEnum | str | int' = Enum_Frame.DATA, - type_default: 'Optional[int]' = None, - type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - type_reversed: 'bool' = False, + type: 'Enum_Frame | int' = Enum_Frame.DATA, flags: 'Flags' = 0, # type: ignore[assignment] reserved: 'int' = 0, sid: 'int' = 0, @@ -427,9 +438,6 @@ def make(self, # type: ignore[override] Args: type: Type of HTTP/2 frame. - type_default: Default frame type. - type_namespace: Namespace of frame type. - type_reversed: Whether to reverse the namespace. flags: Flags of HTTP/2 frame, combined with those the frame constructor derives from ``frame``. reserved: Reserved bit of the frame header. @@ -447,8 +455,10 @@ def make(self, # type: ignore[override] Constructed packet data. Raises: - ProtocolError: If ``frame`` is none of the forms this maker reads -- - raw :obj:`bytes`, a :obj:`dict` of frame arguments, an + ProtocolError: If ``type`` is a name, a :obj:`bool`, or a member + or code the 8-bit Type field cannot carry; or if ``frame`` is + none of the forms this maker reads -- raw :obj:`bytes`, a + :obj:`dict` of frame arguments, an :class:`~pcapkit.protocols.data.application.httpv2.HTTPv2` data model or a :class:`~pcapkit.protocols.schema.schema.Schema`. @@ -463,9 +473,7 @@ def make(self, # type: ignore[override] such a frame could not be built at all (:issue:`2274`). """ - type_val = cast('Enum_Frame', - self._make_index(type, type_default, namespace=type_namespace, - reversed=type_reversed, pack=False)) + type_val = self._make_index(type, Enum_Frame, bits=8, name='type') if frame is None: # A frame type whose payload is of fixed or minimum size cannot be @@ -1364,24 +1372,22 @@ def _make_http_priority(self, frame: 'Optional[Data_PriorityFrame]' = None, *, ), cast('Flags', 0) def _make_http_rst_stream(self, frame: 'Optional[Data_RSTStreamFrame]' = None, *, - error: 'Enum_ErrorCode | str | int | StdlibEnum | AenumEnum' = Enum_ErrorCode.NO_ERROR, - error_default: 'Optional[int]' = None, - error_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - error_reversed: 'bool' = False, + error: 'Enum_ErrorCode | int' = Enum_ErrorCode.NO_ERROR, **kwargs: 'Any') -> 'tuple[Schema_RSTStreamFrame, Flags]': """Make HTTP/2 ``RST_STREAM`` frame. Args: frame: Frame data model. error: Error code, ``NO_ERROR`` unless given. - error_default: Default value of error code. - error_namespace: Namespace of error code. - error_reversed: Reversed namespace of error code. **kwargs: Arbitrary keyword arguments. Returns: Constructed frame schema and updated flags. + Raises: + ProtocolError: If ``error`` is a name, a :obj:`bool`, or a member + or code the 32-bit Error Code field cannot carry. + .. versionchanged:: 1.5.0 ``error`` defaults to ``NO_ERROR``, the code :rfc:`9113#section-7` @@ -1393,8 +1399,7 @@ def _make_http_rst_stream(self, frame: 'Optional[Data_RSTStreamFrame]' = None, * if frame is not None: error_val = frame.error else: - error_val = self._make_index(error, error_default, namespace=error_namespace, # type: ignore[assignment] - reversed=error_reversed, pack=False) + error_val = self._make_index(error, Enum_ErrorCode, bits=32, name='error code') return Schema_RSTStreamFrame( error=error_val, @@ -1402,7 +1407,7 @@ def _make_http_rst_stream(self, frame: 'Optional[Data_RSTStreamFrame]' = None, * def _make_http_settings(self, frame: 'Optional[Data_SettingsFrame]' = None, *, ack: 'bool' = False, - settings: 'Optional[OrderedMultiDict[Enum_Setting, int] | dict[Enum_Setting | int, int] | bytes | list[Schema_SettingPair | tuple[Enum_Setting, int]]]' = None, # pylint: disable=line-too-long + settings: 'Optional[OrderedMultiDict[Enum_Setting | int, int] | dict[Enum_Setting | int, int] | bytes | list[Schema_SettingPair | tuple[Enum_Setting | int, int]]]' = None, # pylint: disable=line-too-long **kwargs: 'Any') -> 'tuple[Schema_SettingsFrame, Flags]': """Make HTTP/2 ``SETTINGS`` frame. @@ -1421,7 +1426,10 @@ def _make_http_settings(self, frame: 'Optional[Data_SettingsFrame]' = None, *, Raises: ProtocolError: If ``settings`` is not :obj:`bytes`, a :obj:`dict`, - a :obj:`list` or :data:`None`. + a :obj:`list` or :data:`None`, or if an identifier other than + a :class:`~pcapkit.protocols.schema.application.httpv2.SettingPair`'s + is a name, a :obj:`bool`, or a member or code the 16-bit + Identifier field cannot carry. .. versionchanged:: 1.5.0 @@ -1451,7 +1459,7 @@ def _make_http_settings(self, frame: 'Optional[Data_SettingsFrame]' = None, *, items = settings.items(multi=True) if isinstance(settings, MultiDict) else settings.items() for key, val in items: settings_val.append(Schema_SettingPair( - id=key, + id=self._make_index(key, Enum_Setting, bits=16, name='setting identifier'), value=val, )) elif isinstance(settings, list): @@ -1462,7 +1470,7 @@ def _make_http_settings(self, frame: 'Optional[Data_SettingsFrame]' = None, *, else: id, value = item temp = Schema_SettingPair( - id=id, + id=self._make_index(id, Enum_Setting, bits=16, name='setting identifier'), value=value, ) settings_val.append(temp) @@ -1563,10 +1571,7 @@ def _make_http_ping(self, frame: 'Optional[Data_PingFrame]' = None, *, def _make_http_goaway(self, frame: 'Optional[Data_GoawayFrame]' = None, *, last_reserved: 'int' = 0, last_sid: 'int' = 0, - error: 'Enum_ErrorCode | str | int | StdlibEnum | AenumEnum' = Enum_ErrorCode.NO_ERROR, - error_default: 'Optional[int]' = None, - error_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - error_reversed: 'bool' = False, + error: 'Enum_ErrorCode | int' = Enum_ErrorCode.NO_ERROR, debug_data: 'bytes' = b'', **kwargs: 'Any') -> 'tuple[Schema_GoawayFrame, Flags]': """Make HTTP/2 ``GOAWAY`` frame. @@ -1576,15 +1581,16 @@ def _make_http_goaway(self, frame: 'Optional[Data_GoawayFrame]' = None, *, last_reserved: Reserved bit of the last stream identifier. last_sid: Last stream identifier. error: Error code, ``NO_ERROR`` unless given. - error_default: Default value of error code. - error_namespace: Namespace of error code. - error_reversed: Reversed namespace of error code. debug_data: Additional debug data. **kwargs: Arbitrary keyword arguments. Returns: Constructed frame schema and updated flags. + Raises: + ProtocolError: If ``error`` is a name, a :obj:`bool`, or a member + or code the 32-bit Error Code field cannot carry. + .. versionchanged:: 1.5.0 ``error`` defaults to ``NO_ERROR``, the code :rfc:`9113#section-7` @@ -1599,8 +1605,7 @@ def _make_http_goaway(self, frame: 'Optional[Data_GoawayFrame]' = None, *, error_val = frame.error debug = frame.debug_data else: - error_val = self._make_index(error, error_default, namespace=error_namespace, # type: ignore[assignment] - reversed=error_reversed, pack=False) + error_val = self._make_index(error, Enum_ErrorCode, bits=32, name='error code') debug = debug_data return Schema_GoawayFrame( diff --git a/pcapkit/protocols/application/ngap.py b/pcapkit/protocols/application/ngap.py index 0b3f3d3061..bc2bbfc70a 100644 --- a/pcapkit/protocols/application/ngap.py +++ b/pcapkit/protocols/application/ngap.py @@ -347,7 +347,17 @@ def _revert(value: 'Any') -> 'Any': class NGAP(Application[Data_NGAP, Schema_NGAP], data=Data_NGAP, schema=Schema_NGAP): - """This class implements NG Application Protocol.""" + """This class implements NG Application Protocol. + + .. versionchanged:: 1.6.0 + + ``procedure`` and ``criticality`` of :meth:`make` take a member or an + :class:`int` code, resolved with the enumeration's ``get``; a name or a + :obj:`bool` raises :exc:`~pcapkit.utilities.exceptions.ProtocolError`, + where a name resolved to its member and a :obj:`bool` was taken as a + code (:issue:`2594`). + + """ ########################################################################## # Properties. @@ -471,7 +481,7 @@ def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_NGAP': def make(self, kind: 'PDUKind | str' = PDUKind.INITIATING_MESSAGE, procedure: 'Optional[Enum_ProcedureCode | int]' = None, - criticality: 'Criticality | int | str' = Criticality.reject, + criticality: 'Criticality | int' = Criticality.reject, message: 'Optional[str]' = None, value: 'Any' = None, data: 'Optional[bytes]' = None, @@ -497,9 +507,11 @@ def make(self, Raises: ProtocolError: If ``data`` is not given and either ``procedure`` or - ``message`` is missing, if |pycrate|_ is not installed, or if - the arguments do not describe a message the specification can - encode. + ``message`` is missing, if ``procedure`` (``0..255``) or + ``criticality`` (two bits) is a name, a :obj:`bool`, or a + member or code its field cannot carry, if |pycrate|_ is not + installed, or if the arguments do not describe a message the + specification can encode. """ if data is not None: @@ -508,6 +520,10 @@ def make(self, if procedure is None or message is None: raise ProtocolError("NGAP: 'procedure' and 'message' are required " "when 'data' is not given") + # NOTE: ProcedureCode is INTEGER (0..255); Criticality, an ENUMERATED of + # three, takes two bits in aligned PER (3GPP TS 38.413). + procedure_ = self._make_index(procedure, Enum_ProcedureCode, bits=8, name='procedure code') + criticality_ = self._make_index(criticality, Criticality, bits=2, name='criticality') pdu = load_pycrate() if pdu is None: @@ -517,8 +533,8 @@ def make(self, with _PDU_LOCK: try: pdu.set_val((PDUKind(kind).value, { - 'procedureCode': int(Enum_ProcedureCode.get(procedure)), - 'criticality': Criticality.get(criticality).name, + 'procedureCode': int(procedure_), + 'criticality': getattr(criticality_, 'name', criticality_), 'value': (message, _revert(value)), })) encoded = pdu.to_aper() diff --git a/pcapkit/protocols/application/ospf.py b/pcapkit/protocols/application/ospf.py index 77648f96c8..fde1bc1091 100644 --- a/pcapkit/protocols/application/ospf.py +++ b/pcapkit/protocols/application/ospf.py @@ -85,11 +85,9 @@ from pcapkit.utilities.exceptions import ProtocolError if TYPE_CHECKING: - from enum import IntEnum as StdlibEnum from ipaddress import IPv4Address - from typing import Any, Optional, Type + from typing import Any, Optional - from aenum import IntEnum as AenumEnum from typing_extensions import Literal from pcapkit.protocols.schema.schema import Schema @@ -117,6 +115,23 @@ class OSPF(Application[Data_OSPF, Schema_OSPF], :attr:`~pcapkit.const.reg.transtype.TransType.OSPFIGP` (IANA protocol number 89), since IP is what carries it. + .. versionchanged:: 1.6.0 + + Each enumeration field of :meth:`make` takes a member or an + :class:`int` code, resolved with the enumeration's ``get``; a name, a + :obj:`bool` for a field wider than one bit, or a code the field cannot + carry raises :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather + than resolving against an enumeration inferred from the caller's frame + or writing a different value (:issue:`2594`). + + .. versionremoved:: 1.6.0 + + The ``type_default``, ``type_namespace``, ``type_reversed``, + ``auth_type_default``, ``auth_type_namespace`` and + ``auth_type_reversed`` arguments of :meth:`make`; resolve a name with + :meth:`~pcapkit.protocols.protocol.Protocol.resolve_index` first + (:issue:`2594`). + """ #: Version number of corresponding protocol, as read off the header. Held on #: the instance rather than read back out of :attr:`self._info @@ -245,18 +260,12 @@ def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_OSPF': def make(self, version: 'int' = 2, - type: 'Enum_Packet | StdlibEnum | AenumEnum | str | int' = Enum_Packet.Hello, - type_default: 'Optional[int]' = None, - type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - type_reversed: 'bool' = False, + type: 'Enum_Packet | int' = Enum_Packet.Hello, packet_length: 'Optional[int]' = None, router_id: 'IPv4Address | str | bytes | bytearray' = '0.0.0.0', # nosec: B104 area_id: 'IPv4Address | str | bytes | bytearray' = '0.0.0.0', # nosec: B104 checksum: 'Optional[bytes]' = None, - auth_type: 'Enum_Authentication | StdlibEnum | AenumEnum | str | int' = Enum_Authentication.No_Authentication, - auth_type_default: 'Optional[int]' = None, - auth_type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - auth_type_reversed: 'bool' = False, + auth_type: 'Enum_Authentication | int' = Enum_Authentication.No_Authentication, auth_data: 'bytes | Schema_CryptographicAuthentication | Data_CryptographicAuthentication | dict[str, Any]' = b'\x00\x00\x00\x00\x00\x00\x00\x00', # pylint: disable=line-too-long payload: 'bytes | ProtocolBase | Schema' = b'', trailer: 'bytes' = b'', @@ -266,9 +275,6 @@ def make(self, Args: version: OSPF version number. type: OSPF packet type. - type_default: Default value for ``type`` if not specified. - type_namespace: Namespace for ``type``. - type_reversed: Reverse namespace for ``type``. packet_length: Packet length (header included). If not given, it is computed as 24 plus the length of ``payload``. Give it explicitly when ``payload`` carries octets outside the OSPF @@ -281,9 +287,6 @@ def make(self, (:rfc:`2328#appendix-A.3.1`); zero otherwise, as cryptographic authentication leaves it (:rfc:`2328#appendix-D.4.3`). auth_type: Authentication type. - auth_type_default: Default value for ``auth_type`` if not specified. - auth_type_namespace: Namespace for ``auth_type``. - auth_type_reversed: Reverse namespace for ``auth_type``. auth_data: Authentication data: 8 octets, or, under cryptographic authentication, also a schema, a data model, or the :obj:`dict` form of a data model. @@ -299,8 +302,10 @@ def make(self, Raises: ProtocolError: If ``auth_data`` is of an invalid type, is :obj:`bytes` that are not exactly 8 octets long, or is a - :obj:`dict` missing a required key; or if ``packet_length`` - is not an :obj:`int` in ``0..65535``. + :obj:`dict` missing a required key; if ``packet_length`` + is not an :obj:`int` in ``0..65535``; or if ``type`` (8 bits) + or ``auth_type`` (16 bits) is a name, a :obj:`bool`, or a + member or code its field cannot carry. .. versionchanged:: 1.5.0 @@ -309,10 +314,10 @@ def make(self, parameter lands one parameter early (:issue:`1249`). """ - type_ = self._make_index(type, type_default, namespace=type_namespace, - reversed=type_reversed, pack=False) - auth_type_ = self._make_index(auth_type, auth_type_default, namespace=auth_type_namespace, - reversed=auth_type_reversed, pack=False) + # NOTE: ``alias`` reads ``_version``, which only ``read()`` sets. + type_ = self._make_index(type, Enum_Packet, bits=8, name='type', alias='OSPF') + auth_type_ = self._make_index(auth_type, Enum_Authentication, bits=16, + name='authentication type', alias='OSPF') data: 'bytes | Schema_CryptographicAuthentication' if auth_type_ == Enum_Authentication.Cryptographic_authentication: @@ -332,12 +337,12 @@ def make(self, schema = Schema_OSPF( version=version, - type=type_, # type: ignore[arg-type] + type=type_, length=packet_length, router_id=router_id, area_id=area_id, checksum=b'\x00\x00' if checksum is None else checksum, - auth_type=auth_type_, # type: ignore[arg-type] + auth_type=auth_type_, auth_data=data, payload=payload, trailer=trailer, diff --git a/pcapkit/protocols/application/ssl.py b/pcapkit/protocols/application/ssl.py index b13c719f46..0f020855b6 100644 --- a/pcapkit/protocols/application/ssl.py +++ b/pcapkit/protocols/application/ssl.py @@ -1494,12 +1494,17 @@ def _make_tls_extensions(self, extensions: 'Iterable[Data_Extension | tuple[Enum Returns: Encoded extension list, with no further length prefix of its own. + Raises: + ProtocolError: If the type of a ``(type, kwargs)`` pair is a name, + a :obj:`bool`, or a member or code the 16-bit extension type + field cannot carry. + """ out = bytearray() for extension in extensions: if isinstance(extension, tuple): code, kwargs = extension - ext_type = Enum_ExtensionType.get(code) + ext_type = self._make_index(code, Enum_ExtensionType, bits=16, name='extension type') data = None # type: Optional[Data_ExtensionData | bytes] else: ext_type, data, kwargs = extension.type, extension.data, {} diff --git a/pcapkit/protocols/application/sslv2.py b/pcapkit/protocols/application/sslv2.py index 72587e5b20..e0d568feb1 100644 --- a/pcapkit/protocols/application/sslv2.py +++ b/pcapkit/protocols/application/sslv2.py @@ -76,10 +76,8 @@ from pcapkit.utilities.exceptions import ProtocolError if TYPE_CHECKING: - from enum import IntEnum as StdlibEnum - from typing import Any, Optional, Type + from typing import Any, Optional - from aenum import IntEnum as AenumEnum from typing_extensions import Literal from pcapkit.protocols.protocol import ProtocolBase @@ -245,10 +243,7 @@ def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_SSLv2': return self._decode_next_layer(sslv2, -1, length - header_len - record_length) def make(self, - msg_type: 'Enum_HandshakeType | StdlibEnum | AenumEnum | str | int' = Enum_HandshakeType.CLIENT_HELLO, - msg_type_default: 'Optional[int]' = None, - msg_type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - msg_type_reversed: 'bool' = False, + msg_type: 'Enum_HandshakeType | int' = Enum_HandshakeType.CLIENT_HELLO, two_byte_header: 'bool' = True, is_escape: 'bool' = False, padding: 'int' = 0, @@ -259,9 +254,6 @@ def make(self, Args: msg_type: Handshake message type. - msg_type_default: Default value for ``msg_type``. - msg_type_namespace: Namespace for ``msg_type``. - msg_type_reversed: Reverse namespace for ``msg_type``. two_byte_header: Whether to emit the 2-octet header form (the more common of the two on real traffic) rather than the 3-octet form. @@ -287,7 +279,11 @@ def make(self, Constructed packet data. Raises: - ProtocolError: If ``body`` is a :obj:`dict` and ``msg_type`` is + ProtocolError: If ``msg_type`` is a name, a :obj:`bool`, or a + member or code the 8-bit ``MSG-TYPE`` field cannot carry, or + a hello body's ``client_version`` or ``server_version`` is + such a value for its 16-bit field; if ``body`` is a + :obj:`dict` and ``msg_type`` is not ``CLIENT_HELLO``/``SERVER_HELLO``; if ``body`` is a :class:`~pcapkit.protocols.data.application.sslv2.ClientHello`/ :class:`~pcapkit.protocols.data.application.sslv2.ServerHello` @@ -300,9 +296,7 @@ def make(self, single padding octet (``0``-``255``). """ - msg_type_val = cast('Enum_HandshakeType', - self._make_index(msg_type, msg_type_default, namespace=msg_type_namespace, - reversed=msg_type_reversed, pack=False)) + msg_type_val = self._make_index(msg_type, Enum_HandshakeType, bits=8, name='msg_type') if body is None: body = {} @@ -406,10 +400,7 @@ def _make_data(cls, data: 'Data_SSLv2') -> 'dict[str, Any]': # type: ignore[ove } def _make_sslv2_client_hello(self, - client_version: 'Enum_Version | StdlibEnum | AenumEnum | str | int' = Enum_Version.SSLv2, # pylint: disable=line-too-long - client_version_default: 'Optional[int]' = None, - client_version_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - client_version_reversed: 'bool' = False, + client_version: 'Enum_Version | int' = Enum_Version.SSLv2, cipher_specs: 'bytes' = b'', session_id: 'bytes' = b'', challenge: 'bytes' = b'\x00' * 16) -> 'Schema_ClientHello': @@ -417,9 +408,6 @@ def _make_sslv2_client_hello(self, Args: client_version: Client's maximum supported version. - client_version_default: Default value for ``client_version``. - client_version_namespace: Namespace for ``client_version``. - client_version_reversed: Reverse namespace for ``client_version``. cipher_specs: Cipher kinds the client supports, as a concatenation of 3-octet ``CIPHER-KIND`` entries. The draft (§5.5.1) requires ``CIPHER-SPECS-LENGTH`` to be greater than @@ -432,11 +420,12 @@ def _make_sslv2_client_hello(self, Returns: ``CLIENT-HELLO`` message body schema. + Raises: + ProtocolError: If ``client_version`` is a name, a :obj:`bool`, or + a member or code its 16-bit field cannot carry. + """ - client_version_val = cast('Enum_Version', - self._make_index(client_version, client_version_default, - namespace=client_version_namespace, - reversed=client_version_reversed, pack=False)) + client_version_val = self._make_index(client_version, Enum_Version, bits=16, name='client_version') return Schema_ClientHello( client_version=client_version_val, cipher_specs_length=len(cipher_specs), @@ -450,10 +439,7 @@ def _make_sslv2_client_hello(self, def _make_sslv2_server_hello(self, session_id_hit: 'int | bool' = 0, certificate_type: 'int' = 0, - server_version: 'Enum_Version | StdlibEnum | AenumEnum | str | int' = Enum_Version.SSLv2, # pylint: disable=line-too-long - server_version_default: 'Optional[int]' = None, - server_version_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - server_version_reversed: 'bool' = False, + server_version: 'Enum_Version | int' = Enum_Version.SSLv2, certificate: 'bytes' = b'', cipher_specs: 'bytes' = b'', connection_id: 'bytes' = b'\x00' * 16) -> 'Schema_ServerHello': @@ -472,9 +458,6 @@ def _make_sslv2_server_hello(self, rather than wrapping mod 256. certificate_type: Certificate type. server_version: Server's selected version. - server_version_default: Default value for ``server_version``. - server_version_namespace: Namespace for ``server_version``. - server_version_reversed: Reverse namespace for ``server_version``. certificate: Server's certificate. cipher_specs: Cipher kinds the server supports, as a concatenation of 3-octet ``CIPHER-KIND`` entries -- empty @@ -489,11 +472,13 @@ def _make_sslv2_server_hello(self, Returns: ``SERVER-HELLO`` message body schema. + Raises: + ProtocolError: If ``server_version`` is a name, a :obj:`bool`, or + a member or code its 16-bit field cannot carry, or if + ``session_id_hit`` does not fit its octet. + """ - server_version_val = cast('Enum_Version', - self._make_index(server_version, server_version_default, - namespace=server_version_namespace, - reversed=server_version_reversed, pack=False)) + server_version_val = self._make_index(server_version, Enum_Version, bits=16, name='server_version') # NOTE: ``int()`` first, so the documented ``True``/``False`` pass; # ``UInt8Field`` packs mod 256, so ``256`` would turn a hit into a miss. session_id_hit_val = self._make_uint(int(session_id_hit), bits=8, name='session_id_hit') diff --git a/pcapkit/protocols/application/tls.py b/pcapkit/protocols/application/tls.py index c710c0746c..6d9cdb4453 100644 --- a/pcapkit/protocols/application/tls.py +++ b/pcapkit/protocols/application/tls.py @@ -64,7 +64,7 @@ .. versionadded:: 1.6.0 """ -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING from pcapkit.const.tls.content_type import ContentType as Enum_ContentType from pcapkit.corekit.decorators import beholder @@ -81,11 +81,8 @@ from pcapkit.utilities.warnings import hold_warnings if TYPE_CHECKING: - from enum import IntEnum as StdlibEnum from typing import Any, Optional - from typing import Type as _Type - from aenum import IntEnum as AenumEnum from typing_extensions import Literal from pcapkit.protocols.protocol import ProtocolBase @@ -225,10 +222,7 @@ def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_TLS': return self._read_tls_record(length, **kwargs) def make(self, - content_type: 'Enum_ContentType | StdlibEnum | AenumEnum | str | int' = Enum_ContentType.application_data, # pylint: disable=line-too-long - content_type_default: 'Optional[int]' = None, - content_type_namespace: 'Optional[dict[str, int] | dict[int, str] | _Type[StdlibEnum] | _Type[AenumEnum]]' = None, # pylint: disable=line-too-long - content_type_reversed: 'bool' = False, + content_type: 'Enum_ContentType | int' = Enum_ContentType.application_data, version: 'int' = 0x0303, content: 'Any' = b'', data: 'Optional[bytes]' = None, @@ -238,9 +232,6 @@ def make(self, Args: content_type: Record content type. - content_type_default: Default value for ``content_type``. - content_type_namespace: Namespace for ``content_type``. - content_type_reversed: Reverse namespace for ``content_type``. version: Legacy record version (``major << 8 | minor``), e.g. ``0x0303`` for TLS 1.2 and (the legacy value TLS 1.3 keeps here) TLS 1.3. @@ -261,18 +252,20 @@ def make(self, Constructed packet data. Raises: - StructError: If a record-header field, or a field of ``content``, - does not fit its width. + ProtocolError: If ``content_type`` is a name, a :obj:`bool`, or a + member or code the 8-bit content type field cannot carry, or + an extension of ``content`` given as a ``(type, kwargs)`` pair + has such a type for its 16-bit field. + StructError: If ``version`` or the record length, or a field of + ``content``, does not fit its width. """ - content_type_ = cast('Enum_ContentType', self._make_index( - content_type, content_type_default, namespace=content_type_namespace, - reversed=content_type_reversed, pack=False)) + content_type_ = self._make_index(content_type, Enum_ContentType, bits=8, name='content type') fragment = data if data is not None else self._make_tls_content(content_type_, content, dtls=False) # NOTE: the schema masks a field to its width, so each is packed here first, to # refuse a wider value with the StructError the content makers raise. - for value, size in ((int(content_type_), 1), (version, 2), (len(fragment), 2)): + for value, size in ((version, 2), (len(fragment), 2)): self._make_pack(value, size=size) return Schema_TLS( content_type=content_type_, diff --git a/pcapkit/protocols/internet/ah.py b/pcapkit/protocols/internet/ah.py index f13039b209..989dda9ebd 100644 --- a/pcapkit/protocols/internet/ah.py +++ b/pcapkit/protocols/internet/ah.py @@ -36,10 +36,8 @@ from pcapkit.utilities.exceptions import UnsupportedCall if TYPE_CHECKING: - from enum import IntEnum as StdlibEnum - from typing import IO, Any, NoReturn, Optional, Type + from typing import IO, Any, NoReturn, Optional - from aenum import IntEnum as AenumEnum from typing_extensions import Literal from pcapkit.corekit.protochain import ProtoChain @@ -71,6 +69,22 @@ class AH(IPsec[Data_AH, Schema_AH], IPv6_Ext[Data_AH, Schema_AH], A rebuild writes the reserved field back as captured rather than as zero (:issue:`1327`). + .. versionchanged:: 1.6.0 + + ``next`` of :meth:`make` takes a member or an :class:`int` code, + resolved with the enumeration's ``get``; a name, a :obj:`bool`, or a + code that does not fit in one octet raises + :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than + resolving against an enumeration inferred from the caller's frame or + writing a different value (:issue:`2594`). + + .. versionremoved:: 1.6.0 + + The ``next_default``, ``next_namespace`` and ``next_reversed`` + arguments of :meth:`make`; resolve a name with + :meth:`~pcapkit.protocols.protocol.Protocol.resolve_index` first + (:issue:`2594`). + """ ########################################################################## @@ -200,10 +214,7 @@ def read(self, length: 'Optional[int]' = None, *, version: 'Literal[4, 6]' = 4, return self._decode_next_layer(ah, schema.next, length - ah.length, declared=False) def make(self, - next: 'Enum_TransType | StdlibEnum | AenumEnum | str | int' = Enum_TransType.UDP, - next_default: 'Optional[int]' = None, - next_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - next_reversed: 'bool' = False, + next: 'Enum_TransType | int' = Enum_TransType.UDP, reserved: 'int' = 0, spi: 'int' = 0, seq: 'int' = 0, @@ -214,9 +225,6 @@ def make(self, Args: next: Next header type. - next_default: Default value of next header type. - next_namespace: Namespace of next header type. - next_reversed: If the namespace is reversed. reserved: Reserved field; zero on send per :rfc:`4302#section-2.3`, but written as given so that a parsed header rebuilds unchanged. spi: Security Parameters Index. @@ -228,18 +236,21 @@ def make(self, Returns: Constructed packet data. + Raises: + ProtocolError: If ``next`` is a name, a :obj:`bool`, or a member + or code that does not fit in one octet. + .. versionchanged:: 1.5.0 Takes ``reserved`` ahead of ``spi`` (:issue:`1327`), which shifts every later positional argument. """ - next_value = self._make_index(next, next_default, namespace=next_namespace, - reversed=next_reversed, pack=False) + next_value = self._make_index(next, Enum_TransType, bits=8, name='next header') length = (len(icv) + AH_HEADER_LEN) // 4 - 2 return Schema_AH( - next=next_value, # type: ignore[arg-type] + next=next_value, len=length, reserved=reserved, spi=spi, diff --git a/pcapkit/protocols/internet/esp.py b/pcapkit/protocols/internet/esp.py index 8598e3582d..e2ab572f6d 100644 --- a/pcapkit/protocols/internet/esp.py +++ b/pcapkit/protocols/internet/esp.py @@ -185,11 +185,9 @@ 'CIPHER_SUITES', 'INTEGRITY_SUITES', 'SecurityAssociation', 'ESPContext'] if TYPE_CHECKING: - from enum import IntEnum as StdlibEnum from ipaddress import IPv4Address, IPv6Address from typing import IO, Any, NoReturn, Optional, Type - from aenum import IntEnum as AenumEnum from typing_extensions import Literal from pcapkit.protocols.protocol import ProtocolBase @@ -1006,6 +1004,24 @@ class ESP(IPsec[Data_ESP, Schema_ESP], IPv6_Ext[Data_ESP, Schema_ESP], extension header". The library follows IANA's registry rather than that sentence (decided on :issue:`895`). + .. versionchanged:: 1.6.0 + + ``next`` of :meth:`make`, written when ``encrypt`` is :data:`True`, + takes a member or an :class:`int` code, resolved with the + enumeration's ``get``; a name, a :obj:`bool`, or a code that does not + fit in one octet raises + :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than + resolving against an enumeration inferred from the caller's frame, + writing a different value or raising another exception + (:issue:`2594`). + + .. versionremoved:: 1.6.0 + + The ``next_default``, ``next_namespace`` and ``next_reversed`` + arguments of :meth:`make`; resolve a name with + :meth:`~pcapkit.protocols.protocol.Protocol.resolve_index` first + (:issue:`2594`). + """ ########################################################################## @@ -1213,10 +1229,7 @@ def read(self, length: 'Optional[int]' = None, *, version: 'Literal[4, 6]' = 4, def make(self, spi: 'int' = 0, seq: 'int' = 0, - next: 'Enum_TransType | StdlibEnum | AenumEnum | str | int' = Enum_TransType.UDP, # pylint: disable=redefined-builtin - next_default: 'Optional[int]' = None, - next_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - next_reversed: 'bool' = False, + next: 'Enum_TransType | int' = Enum_TransType.UDP, # pylint: disable=redefined-builtin encrypt: 'bool' = False, iv: 'Optional[bytes]' = None, pad_len: 'Optional[int]' = None, @@ -1242,9 +1255,6 @@ def make(self, seq: Sequence number. next: Next header type, written into the ESP trailer. Only used when ``encrypt`` is :data:`True`. - next_default: Default value of next header type. - next_namespace: Namespace of next header type. - next_reversed: If the namespace is reversed. encrypt: Whether to protect ``payload``, as described above. iv: Explicit IV to use; a random one is generated when omitted. Only used when ``encrypt`` is :data:`True`. @@ -1261,9 +1271,10 @@ def make(self, Raises: ProtocolError: If ``encrypt`` is :data:`True` and no Security - Association is available for ``spi``, if ``pad_len`` is - outside ``0`` to ``255`` or does not satisfy the algorithm's - alignment requirement, or if + Association is available for ``spi``, ``next`` is a name, a + :obj:`bool`, or a member or code that does not fit in one + octet, or ``pad_len`` is outside ``0`` to ``255`` or does not + satisfy the algorithm's alignment requirement, or if :meth:`SecurityAssociation.encrypt` rejects ``iv`` or cannot run without |cryptography|_. @@ -1283,8 +1294,7 @@ def make(self, raise ProtocolError(f'{self.alias}: no security association for SPI {spi:#010x}; ' f'encrypt=True needs one') - next_value = self._make_index(next, next_default, namespace=next_namespace, - reversed=next_reversed, pack=False) + next_value = self._make_index(next, Enum_TransType, bits=8, name='next header') plain = self._payload_bytes(payload) align = max(association.cipher_suite.block_size, 4) diff --git a/pcapkit/protocols/internet/hip.py b/pcapkit/protocols/internet/hip.py index 70c918630a..39cff05c93 100644 --- a/pcapkit/protocols/internet/hip.py +++ b/pcapkit/protocols/internet/hip.py @@ -215,11 +215,9 @@ if TYPE_CHECKING: from datetime import timedelta - from enum import IntEnum as StdlibEnum from ipaddress import IPv6Address - from typing import IO, Any, Callable, DefaultDict, NoReturn, Optional, Type + from typing import IO, Any, Callable, DefaultDict, NoReturn, Optional - from aenum import IntEnum as AenumEnum from mypy_extensions import DefaultArg, KwArg, NamedArg from typing_extensions import Literal, NotRequired @@ -417,15 +415,31 @@ class HIP(IPv6_Ext[Data_HIP, Schema_HIP], Internet[Data_HIP, Schema_HIP], .. versionchanged:: 1.6.0 - :meth:`make` and the parameter makers, given an integer argument its - field cannot carry -- negative, too wide, or a :obj:`bool` for a field - wider than one bit -- raise - :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than writing - a different value or raising another exception; so does :meth:`make` - for parameters too long for the Header Length, and so do the - ``PUZZLE`` and registration makers for a lifetime that is a - :obj:`bool` or not a finite number of seconds. Registry codes are not - range-checked (:issue:`2614`). + * :meth:`make` and the parameter makers, given an integer argument its + field cannot carry -- negative, too wide, or a :obj:`bool` for a + field wider than one bit -- raise + :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than + writing a different value or raising another exception; so does + :meth:`make` for parameters too long for the Header Length, and so do + the ``PUZZLE`` and registration makers for a lifetime that is a + :obj:`bool` or not a finite number of seconds (:issue:`2614`). + * Each enumeration field of :meth:`make` and of the parameter makers, + and the code of each parameter in ``parameters``, as a ``(code, + value)`` pair or parsed data, takes a member or an :class:`int` code, + resolved with the enumeration's ``get``; a name, a :obj:`bool` for a + field wider than one bit, or a code the field cannot carry raises + :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than + resolving against an enumeration inferred from the caller's frame or + writing a different value (:issue:`2594`). + + .. versionremoved:: 1.6.0 + + The ``*_default``, ``*_namespace`` and ``*_reversed`` arguments that + went with each enumeration field of :meth:`make` and of the parameter + makers, such as ``next_default`` for ``next`` and ``group_default`` for + the items of ``groups``; resolve a name with + :meth:`~pcapkit.protocols.protocol.Protocol.resolve_index` first + (:issue:`2594`). """ @@ -642,14 +656,8 @@ def read(self, length: 'Optional[int]' = None, *, extension: bool = False, **kwa return self._decode_next_layer(hip, schema.next, length - hip.length) def make(self, - next: 'Enum_TransType | StdlibEnum | AenumEnum | str | int' = Enum_TransType.UDP, - next_default: 'Optional[int]' = None, - next_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - next_reversed: 'bool' = False, - packet: 'Enum_Packet | StdlibEnum | AenumEnum | str | int' = Enum_Packet.HIP_DATA, - packet_default: 'Optional[int]' = None, - packet_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - packet_reversed: 'bool' = False, + next: 'Enum_TransType | int' = Enum_TransType.UDP, + packet: 'Enum_Packet | int' = Enum_Packet.HIP_DATA, packet_fixed: 'int' = 0, version: 'int' = 2, version_reserved: 'int' = 0, @@ -659,20 +667,14 @@ def make(self, controls_reserved: 'int' = 0, shit: 'int' = 0, rhit: 'int' = 0, - parameters: 'Optional[list[Schema_Parameter | tuple[Enum_Parameter, Data_Parameter | dict[str, Any]] | bytes] | Parameter]' = None, # pylint: disable=line-too-long + parameters: 'Optional[list[Schema_Parameter | tuple[Enum_Parameter | int, Data_Parameter | dict[str, Any]] | bytes] | Parameter]' = None, # pylint: disable=line-too-long payload: 'bytes | ProtocolBase | Schema' = b'', **kwargs: 'Any') -> 'Schema_HIP': """Make (construct) packet data. Args: next: Next header type. - next_default: Default value for next header type. - next_namespace: Namespace for next header type. - next_reversed: If the next header type is reversed. packet: HIP packet type. - packet_default: Default value for HIP packet type. - packet_namespace: Namespace for HIP packet type. - packet_reversed: If the HIP packet type is reversed. packet_fixed: The fixed leading bit of the ``Packet Type`` octet; :rfc:`7401#section-5.1` prescribes ``0`` when sending. version: HIP version. @@ -693,9 +695,12 @@ def make(self, Constructed packet data. Raises: - ProtocolError: If ``version``, a fixed or reserved bit, ``shit`` or - ``rhit`` does not fit its field, or the parameters make a header - too long for its one-octet Header Length. + ProtocolError: If ``next``, ``packet``, ``version``, a fixed or + reserved bit, ``shit`` or ``rhit`` does not fit its field, + ``next`` or ``packet`` is a name, the code of a parameter in + ``parameters`` is a name, a :obj:`bool` or a code its field + cannot carry, or the parameters make a header too long for its + one-octet Header Length. .. versionchanged:: 1.5.0 @@ -706,10 +711,10 @@ def make(self, argument. """ - next_value = self._make_index(next, next_default, namespace=next_namespace, - reversed=next_reversed, pack=False) - packet_value = self._make_index(packet, packet_default, namespace=packet_namespace, - reversed=packet_reversed, pack=False) + next_value = self._make_index(next, Enum_TransType, bits=8, name='next header', + alias=f'HIPv{version}') + packet_value = self._make_index(packet, Enum_Packet, bits=7, name='packet type', + alias=f'HIPv{version}') if parameters is not None: parameters_value, total_length = self._make_hip_param(parameters, version=version) @@ -722,7 +727,7 @@ def make(self, parameters_value, length = [], 4 return Schema_HIP( - next=next_value, # type: ignore[arg-type] + next=next_value, len=self._make_uint(length, bits=8, name='header length', alias=f'HIPv{version}'), pkt = { 'bit_0': self._make_uint(packet_fixed, bits=1, name='packet type fixed bit', @@ -908,7 +913,7 @@ def _read_hip_param(self, length: 'int', *, version: 'int') -> 'Parameter': # p raise ProtocolError(f'HIPv{version}: invalid format') return options - def _make_hip_param(self, parameters: 'list[Schema_Parameter | tuple[Enum_Parameter, Data_Parameter | dict[str, Any]] | bytes] | Parameter', *, # pylint: disable=line-too-long + def _make_hip_param(self, parameters: 'list[Schema_Parameter | tuple[Enum_Parameter | int, Data_Parameter | dict[str, Any]] | bytes] | Parameter', *, # pylint: disable=line-too-long version: 'int') -> 'tuple[list[Schema_Parameter | bytes], int]': """Make HIP parameter. @@ -926,6 +931,11 @@ def _make_hip_param(self, parameters: 'list[Schema_Parameter | tuple[Enum_Parame Returns: HIP parameters and total length. + Raises: + ProtocolError: If the code of a ``(code, value)`` pair, or the key + of a parameter given as parsed data, is a name, a :obj:`bool` or + a code its field cannot carry. + """ total_length = 0 if isinstance(parameters, list): @@ -944,8 +954,10 @@ def _make_hip_param(self, parameters: 'list[Schema_Parameter | tuple[Enum_Parame parameters_list.append(schema_packed) total_length += len(schema_packed) else: - code, args = cast('tuple[Enum_Parameter, Data_Parameter | dict[str, Any]]', + item, args = cast('tuple[Enum_Parameter | int, Data_Parameter | dict[str, Any]]', schema) + code = self._make_index(item, Enum_Parameter, bits=16, name='parameter type', + alias=f'HIPv{version}') name = self._lookup_registry(self.__parameter__, code) if isinstance(name, str): meth_name = f'_make_param_{name}' @@ -966,7 +978,9 @@ def _make_hip_param(self, parameters: 'list[Schema_Parameter | tuple[Enum_Parame return parameters_list, total_length parameters_list = [] - for code, param in parameters.items(multi=True): + for key, param in parameters.items(multi=True): + code = self._make_index(key, Enum_Parameter, bits=16, name='parameter type', + alias=f'HIPv{version}') name = self._lookup_registry(self.__parameter__, code) if isinstance(name, str): meth_name = f'_make_param_{name}' @@ -3935,10 +3949,7 @@ def _make_param_ack(self, code: 'Enum_Parameter', param: 'Optional[Data_ACKParam def _make_param_dh_group_list(self, code: 'Enum_Parameter', param: 'Optional[Data_DHGroupListParameter]' = None, *, # pylint: disable=unused-argument version: 'int', - groups: 'Optional[list[Enum_Group | StdlibEnum | AenumEnum | str | int]]' = None, - group_default: 'Optional[int]' = None, - group_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - group_reversed: 'bool' = False, + groups: 'Optional[list[Enum_Group | int]]' = None, **kwargs: 'Any') -> 'Schema_DHGroupListParameter': """Make HIP ``DH_GROUP_LIST`` parameter. @@ -3947,16 +3958,14 @@ def _make_param_dh_group_list(self, code: 'Enum_Parameter', param: 'Optional[Dat param: parameter data version: HIP protocol version groups: list of group ID - group_default: default group ID - group_namespace: group ID namespace - group_reversed: reverse group ID namespace **kwargs: arbitrary keyword arguments Returns: HIP parameter schema. Raises: - ProtocolError: The parameter is **NOT** supported in HIPv1. + ProtocolError: The parameter is **NOT** supported in HIPv1, or an + entry of ``groups`` is a name or does not fit its field. """ if version == 1: @@ -3965,20 +3974,9 @@ def _make_param_dh_group_list(self, code: 'Enum_Parameter', param: 'Optional[Dat if param is not None: group_id = cast('list[Enum_Group]', param.group_id) else: - if groups is None: - groups = [] - - # NOTE: ``group`` is a loop variable rather than an argument of this - # maker, so ``_make_index`` has no argument default to infer the - # namespace of a :obj:`str` member name from; name it here rather than - # leave the name unresolvable (#2394). - if group_namespace is None: - group_namespace = Enum_Group - - group_id = [] - for group in groups: - group_id.append(self._make_index(group, group_default, namespace=group_namespace, # type: ignore[arg-type] - reversed=group_reversed, pack=False)) + group_id = [self._make_index(group, Enum_Group, bits=8, name='DH group ID', + alias=f'HIPv{version}') + for group in groups or []] return Schema_DHGroupListParameter( type=code, @@ -3988,10 +3986,7 @@ def _make_param_dh_group_list(self, code: 'Enum_Parameter', param: 'Optional[Dat def _make_param_diffie_hellman(self, code: 'Enum_Parameter', param: 'Optional[Data_DiffieHellmanParameter]' = None, *, # pylint: disable=unused-argument version: 'int', - group: 'Enum_Group | StdlibEnum | AenumEnum | str | int' = Enum_Group.NIST_P_256, - group_default: 'Optional[int]' = None, - group_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - group_reversed: 'bool' = False, + group: 'Enum_Group | int' = Enum_Group.NIST_P_256, pub_val: 'int' = 0, **kwargs: 'Any') -> 'Schema_DiffieHellmanParameter': """Make HIP ``DIFFIE_HELLMAN`` parameter. @@ -4001,9 +3996,6 @@ def _make_param_diffie_hellman(self, code: 'Enum_Parameter', param: 'Optional[Da param: parameter data version: HIP protocol version group: group ID - group_default: default group ID - group_namespace: group ID namespace - group_reversed: reverse group ID namespace pub_val: public value **kwargs: arbitrary keyword arguments @@ -4011,8 +4003,9 @@ def _make_param_diffie_hellman(self, code: 'Enum_Parameter', param: 'Optional[Da HIP parameter schema. Raises: - ProtocolError: If ``pub_val`` is negative, a :obj:`bool`, or wider - than its ``pub_len`` octets. + ProtocolError: If ``group`` is a name or does not fit its field, or + ``pub_val`` is negative, a :obj:`bool`, or wider than its + ``pub_len`` octets. """ if param is not None: @@ -4020,8 +4013,8 @@ def _make_param_diffie_hellman(self, code: 'Enum_Parameter', param: 'Optional[Da pub_len = param.pub_len pub_val = param.pub_val else: - group_id = self._make_index(group, group_default, namespace=group_namespace, # type: ignore[assignment] - reversed=group_reversed, pack=False) + group_id = self._make_index(group, Enum_Group, bits=8, name='group ID', + alias=f'HIPv{version}') pub_len = self._uint_size(pub_val) return Schema_DiffieHellmanParameter( @@ -4035,10 +4028,7 @@ def _make_param_diffie_hellman(self, code: 'Enum_Parameter', param: 'Optional[Da def _make_param_hip_transform(self, code: 'Enum_Parameter', param: 'Optional[Data_HIPTransformParameter]' = None, *, # pylint: disable=unused-argument version: 'int', - suites: 'Optional[list[Enum_Suite | StdlibEnum | AenumEnum | str | int]]' = None, - suite_default: 'Optional[int]' = None, - suite_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - suite_reversed: 'bool' = False, + suites: 'Optional[list[Enum_Suite | int]]' = None, **kwargs: 'Any') -> 'Schema_HIPTransformParameter': """Make HIP ``HIP_TRANSFORM`` parameter. @@ -4047,16 +4037,14 @@ def _make_param_hip_transform(self, code: 'Enum_Parameter', param: 'Optional[Dat param: parameter data version: HIP protocol version suites: list of suite ID - suite_default: default suite ID - suite_namespace: suite ID namespace - suite_reversed: reverse suite ID namespace **kwargs: arbitrary keyword arguments Returns: HIP parameter schema. Raises: - ProtocolError: The parameter is **NOT** supported outside HIPv1. + ProtocolError: The parameter is **NOT** supported outside HIPv1, or + an entry of ``suites`` is a name or does not fit its field. """ # ``HIP_TRANSFORM`` is :rfc:`5201#section-5.2.7` only -- :rfc:`7401` @@ -4070,20 +4058,9 @@ def _make_param_hip_transform(self, code: 'Enum_Parameter', param: 'Optional[Dat if param is not None: suite_id = cast('list[Enum_Suite]', param.suite_id) else: - if suites is None: - suites = [] - - # NOTE: ``suite`` is a loop variable rather than an argument of this - # maker, so ``_make_index`` has no argument default to infer the - # namespace of a :obj:`str` member name from; name it here rather than - # leave the name unresolvable (#2394). - if suite_namespace is None: - suite_namespace = Enum_Suite - - suite_id = [] - for suite in suites: - suite_id.append(self._make_index(suite, suite_default, namespace=suite_namespace, # type: ignore[arg-type] - reversed=suite_reversed, pack=False)) + suite_id = [self._make_index(suite, Enum_Suite, bits=16, name='suite ID', + alias=f'HIPv{version}') + for suite in suites or []] return Schema_HIPTransformParameter( type=code, @@ -4093,10 +4070,7 @@ def _make_param_hip_transform(self, code: 'Enum_Parameter', param: 'Optional[Dat def _make_param_hip_cipher(self, code: 'Enum_Parameter', param: 'Optional[Data_HIPCipherParameter]' = None, *, # pylint: disable=unused-argument version: 'int', - ciphers: 'Optional[list[Enum_Cipher | StdlibEnum | AenumEnum | str | int]]' = None, - cipher_default: 'Optional[int]' = None, - cipher_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - cipher_reversed: 'bool' = False, + ciphers: 'Optional[list[Enum_Cipher | int]]' = None, **kwargs: 'Any') -> 'Schema_HIPCipherParameter': """Make HIP ``HIP_CIPHER`` parameter. @@ -4105,16 +4079,14 @@ def _make_param_hip_cipher(self, code: 'Enum_Parameter', param: 'Optional[Data_H param: parameter data version: HIP protocol version ciphers: list of cipher ID - cipher_default: default cipher ID - cipher_namespace: cipher ID namespace - cipher_reversed: reverse cipher ID namespace **kwargs: arbitrary keyword arguments Returns: HIP parameter schema. Raises: - ProtocolError: The parameter is **NOT** supported in HIPv1. + ProtocolError: The parameter is **NOT** supported in HIPv1, or an + entry of ``ciphers`` is a name or does not fit its field. """ if version == 1: @@ -4123,20 +4095,9 @@ def _make_param_hip_cipher(self, code: 'Enum_Parameter', param: 'Optional[Data_H if param is not None: cipher_id = cast('list[Enum_Cipher]', param.cipher_id) else: - if ciphers is None: - ciphers = [] - - # NOTE: ``cipher`` is a loop variable rather than an argument of this - # maker, so ``_make_index`` has no argument default to infer the - # namespace of a :obj:`str` member name from; name it here rather than - # leave the name unresolvable (#2394). - if cipher_namespace is None: - cipher_namespace = Enum_Cipher - - cipher_id = [] - for cipher in ciphers: - cipher_id.append(self._make_index(cipher, cipher_default, namespace=cipher_namespace, # type: ignore[arg-type] - reversed=cipher_reversed, pack=False)) + cipher_id = [self._make_index(cipher, Enum_Cipher, bits=16, name='cipher ID', + alias=f'HIPv{version}') + for cipher in ciphers or []] return Schema_HIPCipherParameter( type=code, @@ -4146,10 +4107,7 @@ def _make_param_hip_cipher(self, code: 'Enum_Parameter', param: 'Optional[Data_H def _make_param_nat_traversal_mode(self, code: 'Enum_Parameter', param: 'Optional[Data_NATTraversalModeParameter]' = None, *, # pylint: disable=unused-argument version: 'int', - modes: 'Optional[list[Enum_NATTraversal | StdlibEnum | AenumEnum | str | int]]' = None, - mode_default: 'Optional[int]' = None, - mode_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - mode_reversed: 'bool' = False, + modes: 'Optional[list[Enum_NATTraversal | int]]' = None, reserved: 'bytes' = b'\x00\x00', **kwargs: 'Any') -> 'Schema_NATTraversalModeParameter': """Make HIP ``NAT_TRAVERSAL_MODE`` parameter. @@ -4159,9 +4117,6 @@ def _make_param_nat_traversal_mode(self, code: 'Enum_Parameter', param: 'Optiona param: parameter data version: HIP protocol version modes: list of mode ID - mode_default: default mode ID - mode_namespace: mode ID namespace - mode_reversed: reverse mode ID namespace reserved: the two reserved octets, truncated or zero-filled to 2 octets **kwargs: arbitrary keyword arguments @@ -4169,25 +4124,18 @@ def _make_param_nat_traversal_mode(self, code: 'Enum_Parameter', param: 'Optiona Returns: HIP parameter schema. + Raises: + ProtocolError: If an entry of ``modes`` is a name or does not fit + its field. + """ if param is not None: reserved = getattr(param, 'reserved', b'\x00\x00') mode_id = cast('list[Enum_NATTraversal]', param.mode_id) else: - if modes is None: - modes = [] - - # NOTE: ``mode`` is a loop variable rather than an argument of this - # maker, so ``_make_index`` has no argument default to infer the - # namespace of a :obj:`str` member name from; name it here rather than - # leave the name unresolvable (#2394). - if mode_namespace is None: - mode_namespace = Enum_NATTraversal - - mode_id = [] - for mode in modes: - mode_id.append(self._make_index(mode, mode_default, namespace=mode_namespace, # type: ignore[arg-type] - reversed=mode_reversed, pack=False)) + mode_id = [self._make_index(mode, Enum_NATTraversal, bits=16, name='mode ID', + alias=f'HIPv{version}') + for mode in modes or []] return Schema_NATTraversalModeParameter( reserved=reserved, @@ -4227,10 +4175,7 @@ def _make_param_transaction_pacing(self, code: 'Enum_Parameter', param: 'Optiona def _make_param_encrypted(self, code: 'Enum_Parameter', param: 'Optional[Data_EncryptedParameter]' = None, *, # pylint: disable=unused-argument version: 'int', - cipher: 'Enum_Cipher | StdlibEnum | AenumEnum | str | int' = Enum_Cipher.NULL_ENCRYPT, - cipher_default: 'Optional[int]' = None, - cipher_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - cipher_reversed: 'bool' = False, + cipher: 'Enum_Cipher | int' = Enum_Cipher.NULL_ENCRYPT, iv: 'Optional[bytes]' = None, data: 'bytes' = b'', reserved: 'bytes' = b'\x00\x00\x00\x00', @@ -4241,10 +4186,7 @@ def _make_param_encrypted(self, code: 'Enum_Parameter', param: 'Optional[Data_En code: parameter code param: parameter data version: HIP protocol version - cipher: cipher ID - cipher_default: default cipher ID - cipher_namespace: cipher ID namespace - cipher_reversed: reverse cipher ID namespace + cipher: cipher ID, of the width ``HIP_CIPHER`` carries it in iv: initialization vector (optional depending on cipher ID) data: encrypted data reserved: the four reserved octets, truncated or @@ -4255,8 +4197,9 @@ def _make_param_encrypted(self, code: 'Enum_Parameter', param: 'Optional[Data_En HIP parameter schema. Raises: - ProtocolError: If the cipher is ``AES-128-CBC`` or ``AES-256-CBC`` - and ``iv`` is missing or **NOT** 16 octets. + ProtocolError: If ``cipher`` is a name or does not fit its field, + or the cipher is ``AES-128-CBC`` or ``AES-256-CBC`` and ``iv`` + is missing or **NOT** 16 octets. """ if param is not None: @@ -4265,8 +4208,8 @@ def _make_param_encrypted(self, code: 'Enum_Parameter', param: 'Optional[Data_En iv = param.iv data = param.data else: - cipher_id = self._make_index(cipher, cipher_default, namespace=cipher_namespace, # type: ignore[assignment] - reversed=cipher_reversed, pack=False) + cipher_id = self._make_index(cipher, Enum_Cipher, bits=16, name='cipher ID', + alias=f'HIPv{version}') if cipher_id in (Enum_Cipher.AES_128_CBC, Enum_Cipher.AES_256_CBC): if iv is None: @@ -4299,15 +4242,9 @@ def _make_param_host_id(self, code: 'Enum_Parameter', param: 'Optional[Data_Host hi: 'Optional[Data_HostIdentity | bytes | Schema_HostIdentity]' = None, hi_curve: 'Optional[Enum_ECDSACurve | Enum_ECDSALowCurve | Enum_EdDSACurve]' = None, # pylint: disable=line-too-long hi_pub_key: 'bytes' = b'', - hi_algorithm: 'Enum_HIAlgorithm | StdlibEnum | AenumEnum | str | int' = Enum_HIAlgorithm.NULL_ENCRYPT, - hi_algorithm_default: 'Optional[int]' = None, - hi_algorithm_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - hi_algorithm_reversed: 'bool' = False, + hi_algorithm: 'Enum_HIAlgorithm | int' = Enum_HIAlgorithm.NULL_ENCRYPT, di: 'bytes' = b'', - di_type: 'Enum_DITypes | StdlibEnum | AenumEnum | str | int' = Enum_DITypes.none_included, - di_type_default: 'Optional[int]' = None, - di_type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - di_type_reversed: 'bool' = False, + di_type: 'Enum_DITypes | int' = Enum_DITypes.none_included, **kwargs: 'Any') -> 'Schema_HostIDParameter': """Make HIP ``HOST_ID`` parameter. @@ -4319,21 +4256,16 @@ def _make_param_host_id(self, code: 'Enum_Parameter', param: 'Optional[Data_Host hi_curve: host identity curve hi_pub_key: host identity public key hi_algorithm: host identity algorithm - hi_algorithm_default: default host identity algorithm - hi_algorithm_namespace: host identity algorithm namespace - hi_algorithm_reversed: reverse host identity algorithm namespace di: domain identifier di_type: domain identifier type - di_type_default: default domain identifier type - di_type_namespace: domain identifier type namespace - di_type_reversed: reverse domain identifier type namespace **kwargs: arbitrary keyword arguments Returns: HIP parameter schema. Raises: - ProtocolError: If neither ``hi`` nor ``param`` is given and + ProtocolError: If ``hi_algorithm`` or ``di_type`` is a name or does + not fit its field, if neither ``hi`` nor ``param`` is given and ``hi_curve`` is not an ECDSA, ECDSA_LOW or EdDSA curve, or if ``hi`` is a :class:`~pcapkit.protocols.data.internet.hip.HostIdentity` whose ``curve`` is none of those. @@ -4348,10 +4280,10 @@ def _make_param_host_id(self, code: 'Enum_Parameter', param: 'Optional[Data_Host algorithm = param.algorithm # type: int | Enum_HIAlgorithm else: di_len = len(di) - di_enum = self._make_index(di_type, di_type_default, namespace=di_type_namespace, # type: ignore[assignment] - reversed=di_type_reversed, pack=False) - algorithm = self._make_index(hi_algorithm, hi_algorithm_default, namespace=hi_algorithm_namespace, - reversed=hi_algorithm_reversed, pack=False) + di_enum = self._make_index(di_type, Enum_DITypes, bits=4, name='DI-type', + alias=f'HIPv{version}') + algorithm = self._make_index(hi_algorithm, Enum_HIAlgorithm, bits=16, name='algorithm', + alias=f'HIPv{version}') if hi is None: hi_len = 2 + len(hi_pub_key) @@ -4421,10 +4353,7 @@ def _make_param_host_id(self, code: 'Enum_Parameter', param: 'Optional[Data_Host def _make_param_hit_suite_list(self, code: 'Enum_Parameter', param: 'Optional[Data_HITSuiteListParameter]' = None, *, # pylint: disable=unused-argument version: 'int', - suites: 'Optional[list[Enum_HITSuite | StdlibEnum | AenumEnum | str | int]]' = None, - suite_default: 'Optional[int]' = None, - suite_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - suite_reversed: 'bool' = False, + suites: 'Optional[list[Enum_HITSuite | int]]' = None, **kwargs: 'Any') -> 'Schema_HITSuiteListParameter': """Make HIP ``HIT_SUITE_LIST`` parameter. @@ -4433,16 +4362,14 @@ def _make_param_hit_suite_list(self, code: 'Enum_Parameter', param: 'Optional[Da param: parameter data version: HIP protocol version suites: list of suites - suite_default: default suite - suite_namespace: suite namespace - suite_reversed: reverse suite namespace **kwargs: arbitrary keyword arguments Returns: HIP parameter schema. Raises: - ProtocolError: The parameter is **NOT** supported in HIPv1. + ProtocolError: The parameter is **NOT** supported in HIPv1, or an + entry of ``suites`` is a name or does not fit its field. """ if version == 1: @@ -4451,20 +4378,9 @@ def _make_param_hit_suite_list(self, code: 'Enum_Parameter', param: 'Optional[Da if param is not None: suite_id = cast('list[Enum_HITSuite]', param.suite_id) else: - if suites is None: - suites = [] - - # NOTE: ``suite`` is a loop variable rather than an argument of this - # maker, so ``_make_index`` has no argument default to infer the - # namespace of a :obj:`str` member name from; name it here rather than - # leave the name unresolvable (#2394). - if suite_namespace is None: - suite_namespace = Enum_HITSuite - - suite_id = [] - for suite in suites: - suite_id.append(self._make_index(suite, suite_default, namespace=suite_namespace, # type: ignore[arg-type] - reversed=suite_reversed, pack=False)) + suite_id = [self._make_index(suite, Enum_HITSuite, bits=8, name='HIT suite ID', + alias=f'HIPv{version}') + for suite in suites or []] return Schema_HITSuiteListParameter( type=code, @@ -4477,10 +4393,7 @@ def _make_param_cert(self, code: 'Enum_Parameter', param: 'Optional[Data_CertPar cert_group: 'int' = 0, cert_count: 'int' = 0, cert_id: 'int' = 0, - cert_type: 'Enum_Certificate | StdlibEnum | AenumEnum | str | int' = Enum_Certificate.X_509_v3, - cert_type_default: 'Optional[int]' = None, - cert_type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - cert_type_reversed: 'bool' = False, + cert_type: 'Enum_Certificate | int' = Enum_Certificate.X_509_v3, cert: 'bytes' = b'', **kwargs: 'Any') -> 'Schema_CertParameter': """Make HIP ``CERT`` parameter. @@ -4494,9 +4407,6 @@ def _make_param_cert(self, code: 'Enum_Parameter', param: 'Optional[Data_CertPar cert_count: certificate count cert_id: certificate ID cert_type: certificate type - cert_type_default: default certificate type - cert_type_namespace: certificate type namespace - cert_type_reversed: reverse certificate type namespace cert: certificate data **kwargs: arbitrary keyword arguments @@ -4504,8 +4414,8 @@ def _make_param_cert(self, code: 'Enum_Parameter', param: 'Optional[Data_CertPar HIP parameter schema. Raises: - ProtocolError: If ``cert_group``, ``cert_count`` or ``cert_id`` does not fit - its field. + ProtocolError: If ``cert_group``, ``cert_count``, ``cert_id`` or + ``cert_type`` does not fit its field, or ``cert_type`` is a name. .. versionchanged:: 1.5.0 @@ -4526,8 +4436,8 @@ def _make_param_cert(self, code: 'Enum_Parameter', param: 'Optional[Data_CertPar group = cert_group count = cert_count id = cert_id - type = self._make_index(cert_type, cert_type_default, namespace=cert_type_namespace, # type: ignore[assignment] - reversed=cert_type_reversed, pack=False) + type = self._make_index(cert_type, Enum_Certificate, bits=8, name='certificate type', + alias=f'HIPv{version}') return Schema_CertParameter( type=code, @@ -4543,10 +4453,7 @@ def _make_param_cert(self, code: 'Enum_Parameter', param: 'Optional[Data_CertPar def _make_param_notification(self, code: 'Enum_Parameter', param: 'Optional[Data_NotificationParameter]' = None, *, # pylint: disable=unused-argument version: 'int', - msg_type: 'Enum_NotifyMessage | StdlibEnum | AenumEnum | str | int' = Enum_NotifyMessage.I2_ACKNOWLEDGEMENT, - msg_type_default: 'Optional[int]' = None, - msg_type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - msg_type_reversed: 'bool' = False, + msg_type: 'Enum_NotifyMessage | int' = Enum_NotifyMessage.I2_ACKNOWLEDGEMENT, msg: 'bytes' = b'', reserved: 'bytes' = b'\x00\x00', **kwargs: 'Any') -> 'Schema_NotificationParameter': @@ -4557,9 +4464,6 @@ def _make_param_notification(self, code: 'Enum_Parameter', param: 'Optional[Data param: parameter data version: HIP protocol version msg_type: notification message type - msg_type_default: default notification message type - msg_type_namespace: notification message type namespace - msg_type_reversed: reverse notification message type namespace msg: notification message reserved: the two reserved octets, truncated or zero-filled to 2 octets @@ -4568,14 +4472,17 @@ def _make_param_notification(self, code: 'Enum_Parameter', param: 'Optional[Data Returns: HIP parameter schema. + Raises: + ProtocolError: If ``msg_type`` is a name or does not fit its field. + """ if param is not None: reserved = getattr(param, 'reserved', b'\x00\x00') type = param.msg_type msg = param.msg else: - type = self._make_index(msg_type, msg_type_default, namespace=msg_type_namespace, # type: ignore[assignment] - reversed=msg_type_reversed, pack=False) + type = self._make_index(msg_type, Enum_NotifyMessage, bits=16, + name='notify message type', alias=f'HIPv{version}') return Schema_NotificationParameter( reserved=reserved, @@ -4617,10 +4524,7 @@ def _make_param_reg_info(self, code: 'Enum_Parameter', param: 'Optional[Data_Reg max_lifetime: 'timedelta | int | float' = 0xf, min_lifetime_exponent: 'Optional[int]' = None, max_lifetime_exponent: 'Optional[int]' = None, - reg_info: 'Optional[list[Enum_Registration | StdlibEnum | AenumEnum | str | int]]' = None, - reg_info_default: 'Optional[int]' = None, - reg_info_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - reg_info_reversed: 'bool' = False, + reg_info: 'Optional[list[Enum_Registration | int]]' = None, **kwargs: 'Any') -> 'Schema_RegInfoParameter': """Make HIP ``REG_INFO`` parameter. @@ -4639,18 +4543,16 @@ def _make_param_reg_info(self, code: 'Enum_Parameter', param: 'Optional[Data_Reg ``2^((lifetime - 64)/8)`` seconds max_lifetime_exponent: the ``Max Lifetime`` octet itself reg_info: registration information list - reg_info_default: default registration information - reg_info_namespace: registration information namespace - reg_info_reversed: reverse registration information namespace **kwargs: arbitrary keyword arguments Returns: HIP parameter schema. Raises: - ProtocolError: If either exponent does not fit one octet, or either - lifetime cannot be encoded; see - ``_make_registration_lifetime``. + ProtocolError: If either exponent does not fit one octet, either + lifetime cannot be encoded, see + ``_make_registration_lifetime``, or an entry of ``reg_info`` is a + name or does not fit its field. .. versionchanged:: 1.5.0 @@ -4667,20 +4569,9 @@ def _make_param_reg_info(self, code: 'Enum_Parameter', param: 'Optional[Data_Reg if max_lifetime_exponent is None: max_lifetime_exponent = param.lifetime.max_exponent else: - if reg_info is None: - reg_info = [] - - # NOTE: ``reg`` is a loop variable rather than an argument of this - # maker, so ``_make_index`` has no argument default to infer the - # namespace of a :obj:`str` member name from; name it here rather than - # leave the name unresolvable (#2394). - if reg_info_namespace is None: - reg_info_namespace = Enum_Registration - - reg_type = [] - for reg in reg_info: - reg_type.append(self._make_index(reg, reg_info_default, namespace=reg_info_namespace, # type: ignore[arg-type] - reversed=reg_info_reversed, pack=False)) + reg_type = [self._make_index(reg, Enum_Registration, bits=8, name='registration type', + alias=f'HIPv{version}') + for reg in reg_info or []] min_lt = self._make_lifetime_octet(code, version, min_lifetime_exponent, min_lifetime) max_lt = self._make_lifetime_octet(code, version, max_lifetime_exponent, max_lifetime) @@ -4697,10 +4588,7 @@ def _make_param_reg_request(self, code: 'Enum_Parameter', param: 'Optional[Data_ version: 'int', lifetime: 'timedelta | int | float' = 0, lifetime_exponent: 'Optional[int]' = None, - reg_request: 'Optional[list[Enum_Registration | StdlibEnum | AenumEnum | str | int]]' = None, - reg_request_default: 'Optional[int]' = None, - reg_request_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - reg_request_reversed: 'bool' = False, + reg_request: 'Optional[list[Enum_Registration | int]]' = None, **kwargs: 'Any') -> 'Schema_RegRequestParameter': """Make HIP ``REG_REQUEST`` parameter. @@ -4713,18 +4601,16 @@ def _make_param_reg_request(self, code: 'Enum_Parameter', param: 'Optional[Data_ lifetime_exponent: the ``Lifetime`` octet itself, ``lifetime`` in :rfc:`8003#section-4.1`'s ``2^((lifetime - 64)/8)`` seconds reg_request: registration request list - reg_request_default: default registration request - reg_request_namespace: registration request namespace - reg_request_reversed: reverse registration request namespace **kwargs: arbitrary keyword arguments Returns: HIP parameter schema. Raises: - ProtocolError: If ``lifetime_exponent`` does not fit one octet, or - ``lifetime`` cannot be encoded; see - ``_make_registration_lifetime``. + ProtocolError: If ``lifetime_exponent`` does not fit one octet, + ``lifetime`` cannot be encoded, see + ``_make_registration_lifetime``, or an entry of ``reg_request`` + is a name or does not fit its field. .. versionchanged:: 1.5.0 @@ -4738,20 +4624,9 @@ def _make_param_reg_request(self, code: 'Enum_Parameter', param: 'Optional[Data_ if lifetime_exponent is None: lifetime_exponent = param.lifetime_exponent else: - if reg_request is None: - reg_request = [] - - # NOTE: ``reg`` is a loop variable rather than an argument of this - # maker, so ``_make_index`` has no argument default to infer the - # namespace of a :obj:`str` member name from; name it here rather than - # leave the name unresolvable (#2394). - if reg_request_namespace is None: - reg_request_namespace = Enum_Registration - - reg_type = [] - for reg in reg_request: - reg_type.append(self._make_index(reg, reg_request_default, namespace=reg_request_namespace, # type: ignore[arg-type] - reversed=reg_request_reversed, pack=False)) + reg_type = [self._make_index(reg, Enum_Registration, bits=8, name='registration type', + alias=f'HIPv{version}') + for reg in reg_request or []] lt = self._make_lifetime_octet(code, version, lifetime_exponent, lifetime) @@ -4766,10 +4641,7 @@ def _make_param_reg_response(self, code: 'Enum_Parameter', param: 'Optional[Data version: 'int', lifetime: 'timedelta | int | float' = 0, lifetime_exponent: 'Optional[int]' = None, - reg_response: 'Optional[list[Enum_Registration | StdlibEnum | AenumEnum | str | int]]' = None, - reg_response_default: 'Optional[int]' = None, - reg_response_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - reg_response_reversed: 'bool' = False, + reg_response: 'Optional[list[Enum_Registration | int]]' = None, **kwargs: 'Any') -> 'Schema_RegResponseParameter': """Make HIP ``REG_RESPONSE`` parameter. @@ -4782,18 +4654,16 @@ def _make_param_reg_response(self, code: 'Enum_Parameter', param: 'Optional[Data lifetime_exponent: the ``Lifetime`` octet itself, ``lifetime`` in :rfc:`8003#section-4.1`'s ``2^((lifetime - 64)/8)`` seconds reg_response: registration response list - reg_response_default: default registration response - reg_response_namespace: registration response namespace - reg_response_reversed: reverse registration response namespace **kwargs: arbitrary keyword arguments Returns: HIP parameter schema. Raises: - ProtocolError: If ``lifetime_exponent`` does not fit one octet, or - ``lifetime`` cannot be encoded; see - ``_make_registration_lifetime``. + ProtocolError: If ``lifetime_exponent`` does not fit one octet, + ``lifetime`` cannot be encoded, see + ``_make_registration_lifetime``, or an entry of + ``reg_response`` is a name or does not fit its field. .. versionchanged:: 1.5.0 @@ -4807,20 +4677,9 @@ def _make_param_reg_response(self, code: 'Enum_Parameter', param: 'Optional[Data if lifetime_exponent is None: lifetime_exponent = param.lifetime_exponent else: - if reg_response is None: - reg_response = [] - - # NOTE: ``reg`` is a loop variable rather than an argument of this - # maker, so ``_make_index`` has no argument default to infer the - # namespace of a :obj:`str` member name from; name it here rather than - # leave the name unresolvable (#2394). - if reg_response_namespace is None: - reg_response_namespace = Enum_Registration - - reg_type = [] - for reg in reg_response: - reg_type.append(self._make_index(reg, reg_response_default, namespace=reg_response_namespace, # type: ignore[arg-type] - reversed=reg_response_reversed, pack=False)) + reg_type = [self._make_index(reg, Enum_Registration, bits=8, name='registration type', + alias=f'HIPv{version}') + for reg in reg_response or []] lt = self._make_lifetime_octet(code, version, lifetime_exponent, lifetime) @@ -4833,14 +4692,8 @@ def _make_param_reg_response(self, code: 'Enum_Parameter', param: 'Optional[Data def _make_param_reg_failed(self, code: 'Enum_Parameter', param: 'Optional[Data_RegFailedParameter]' = None, *, # pylint: disable=unused-argument version: 'int', - failure_type: 'Enum_RegistrationFailure | StdlibEnum | AenumEnum | str | int' = Enum_RegistrationFailure.Registration_requires_additional_credentials, # pylint: disable=line-too-long - failure_type_default: 'Optional[int]' = None, - failure_type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - failure_type_reversed: 'bool' = False, - reg_failed: 'Optional[list[Enum_Registration | StdlibEnum | AenumEnum | str | int]]' = None, - reg_failed_default: 'Optional[int]' = None, - reg_failed_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - reg_failed_reversed: 'bool' = False, + failure_type: 'Enum_RegistrationFailure | int' = Enum_RegistrationFailure.Registration_requires_additional_credentials, # pylint: disable=line-too-long + reg_failed: 'Optional[list[Enum_Registration | int]]' = None, **kwargs: 'Any') -> 'Schema_RegFailedParameter': """Make HIP ``REG_FAILED`` parameter. @@ -4855,42 +4708,26 @@ def _make_param_reg_failed(self, code: 'Enum_Parameter', param: 'Optional[Data_R param: parameter data version: HIP protocol version failure_type: registration failure type - failure_type_default: default registration failure type - failure_type_namespace: registration failure type namespace - failure_type_reversed: reverse registration failure type namespace reg_failed: list of registration types that failed - reg_failed_default: default registration type - reg_failed_namespace: registration type namespace - reg_failed_reversed: reverse registration type namespace **kwargs: arbitrary keyword arguments Returns: HIP parameter schema. + Raises: + ProtocolError: If ``failure_type`` or an entry of ``reg_failed`` is + a name or does not fit its field. + """ if param is not None: failure = param.failure_type reg_type = cast('list[Enum_Registration]', param.reg_type) else: - failure = self._make_index(failure_type, failure_type_default, namespace=failure_type_namespace, # type: ignore[assignment] - reversed=failure_type_reversed, pack=False) - - if reg_failed is None: - reg_failed = [] - - # NOTE: ``reg`` is a loop variable rather than an argument of this - # maker, so ``_make_index`` has no argument default to infer the - # namespace of a :obj:`str` member name from; name it here rather than - # leave the name unresolvable (#2394). ``failure_type`` above is a - # scalar whose own default is an enumeration member, so it has been - # resolving a member name by inference all along. - if reg_failed_namespace is None: - reg_failed_namespace = Enum_Registration - - reg_type = [] - for reg in reg_failed: - reg_type.append(self._make_index(reg, reg_failed_default, namespace=reg_failed_namespace, # type: ignore[arg-type] - reversed=reg_failed_reversed, pack=False)) + failure = self._make_index(failure_type, Enum_RegistrationFailure, bits=8, + name='failure type', alias=f'HIPv{version}') + reg_type = [self._make_index(reg, Enum_Registration, bits=8, name='registration type', + alias=f'HIPv{version}') + for reg in reg_failed or []] return Schema_RegFailedParameter( type=code, @@ -4902,10 +4739,7 @@ def _make_param_reg_failed(self, code: 'Enum_Parameter', param: 'Optional[Data_R def _make_param_reg_from(self, code: 'Enum_Parameter', param: 'Optional[Data_RegFromParameter]' = None, *, # pylint: disable=unused-argument version: 'int', port: 'int' = 0, - protocol: 'Enum_TransType | StdlibEnum | AenumEnum | str | int' = Enum_TransType.UDP, - protocol_default: 'Optional[int]' = None, - protocol_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - protocol_reversed: 'bool' = False, + protocol: 'Enum_TransType | int' = Enum_TransType.UDP, address: 'IPv6Address | str | int | bytes' = '::', reserved: 'bytes' = b'\x00', **kwargs: 'Any') -> 'Schema_RegFromParameter': @@ -4917,9 +4751,6 @@ def _make_param_reg_from(self, code: 'Enum_Parameter', param: 'Optional[Data_Reg version: HIP protocol version port: port number protocol: transport protocol - protocol_default: default transport protocol - protocol_namespace: transport protocol namespace - protocol_reversed: reverse transport protocol namespace address: IPv6 address reserved: the reserved octet, truncated or zero-filled to 1 octet @@ -4929,7 +4760,8 @@ def _make_param_reg_from(self, code: 'Enum_Parameter', param: 'Optional[Data_Reg HIP parameter schema. Raises: - ProtocolError: If ``port`` does not fit its field. + ProtocolError: If ``port`` or ``protocol`` does not fit its field, + or ``protocol`` is a name. """ if param is not None: @@ -4938,8 +4770,8 @@ def _make_param_reg_from(self, code: 'Enum_Parameter', param: 'Optional[Data_Reg proto = param.protocol address = param.address else: - proto = self._make_index(protocol, protocol_default, namespace=protocol_namespace, # type: ignore[assignment] - reversed=protocol_reversed, pack=False) + proto = self._make_index(protocol, Enum_TransType, bits=8, name='protocol', + alias=f'HIPv{version}') return Schema_RegFromParameter( reserved=reserved, @@ -4978,10 +4810,7 @@ def _make_param_echo_response_signed(self, code: 'Enum_Parameter', param: 'Optio def _make_param_transport_format_list(self, code: 'Enum_Parameter', param: 'Optional[Data_TransportFormatListParameter]' = None, *, # pylint: disable=unused-argument version: 'int', - formats: 'Optional[list[Enum_Parameter | StdlibEnum | AenumEnum | str | int]]' = None, - format_default: 'Optional[int]' = None, - format_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - format_reversed: 'bool' = False, + formats: 'Optional[list[Enum_Parameter | int]]' = None, **kwargs: 'Any') -> 'Schema_TransportFormatListParameter': """Make HIP ``TRANSPORT_FORMAT_LIST`` parameter. @@ -4989,17 +4818,15 @@ def _make_param_transport_format_list(self, code: 'Enum_Parameter', param: 'Opti code: parameter code param: parameter data version: HIP protocol version - formats: transport format list - format_default: default transport format - format_namespace: transport format namespace - format_reversed: reverse transport format namespace + formats: transport format list, each a parameter type **kwargs: arbitrary keyword arguments Returns: HIP parameter schema. Raises: - ProtocolError: The parameter is **NOT** supported in HIPv1. + ProtocolError: The parameter is **NOT** supported in HIPv1, or an + entry of ``formats`` is a name or does not fit its field. """ if version == 1: @@ -5008,21 +4835,9 @@ def _make_param_transport_format_list(self, code: 'Enum_Parameter', param: 'Opti if param is not None: tf_type = cast('list[Enum_Parameter]', param.tf_type) else: - if formats is None: - formats = [] - - # NOTE: ``tf`` is a loop variable rather than an argument of this - # maker, so ``_make_index`` has no argument default to infer the - # namespace of a :obj:`str` member name from; name it here rather than - # leave the name unresolvable (#2394). A transport format is a - # parameter code, so the namespace is ``Enum_Parameter`` itself. - if format_namespace is None: - format_namespace = Enum_Parameter - - tf_type = [] - for tf in formats: - tf_type.append(self._make_index(tf, format_default, namespace=format_namespace, # type: ignore[arg-type] - reversed=format_reversed, pack=False)) + tf_type = [self._make_index(tf, Enum_Parameter, bits=16, name='transport format type', + alias=f'HIPv{version}') + for tf in formats or []] return Schema_TransportFormatListParameter( type=code, @@ -5032,10 +4847,7 @@ def _make_param_transport_format_list(self, code: 'Enum_Parameter', param: 'Opti def _make_param_esp_transform(self, code: 'Enum_Parameter', param: 'Optional[Data_ESPTransformParameter]' = None, *, # pylint: disable=unused-argument version: 'int', - suites: 'Optional[list[Enum_ESPTransformSuite | StdlibEnum | AenumEnum | str | int]]' = None, - suite_default: 'Optional[int]' = None, - suite_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - suite_reversed: 'bool' = False, + suites: 'Optional[list[Enum_ESPTransformSuite | int]]' = None, reserved: 'bytes' = b'\x00\x00', **kwargs: 'Any') -> 'Schema_ESPTransformParameter': """Make HIP ``ESP_TRANSFORM`` parameter. @@ -5045,9 +4857,6 @@ def _make_param_esp_transform(self, code: 'Enum_Parameter', param: 'Optional[Dat param: parameter data version: HIP protocol version suites: ESP transform suite list - suite_default: default ESP transform suite - suite_namespace: ESP transform suite namespace - suite_reversed: reverse ESP transform suite namespace reserved: the two reserved octets, truncated or zero-filled to 2 octets **kwargs: arbitrary keyword arguments @@ -5055,25 +4864,18 @@ def _make_param_esp_transform(self, code: 'Enum_Parameter', param: 'Optional[Dat Returns: HIP parameter schema. + Raises: + ProtocolError: If an entry of ``suites`` is a name or does not fit + its field. + """ if param is not None: reserved = getattr(param, 'reserved', b'\x00\x00') suite_id = cast('list[Enum_ESPTransformSuite]', param.suite_id) else: - if suites is None: - suites = [] - - # NOTE: ``suite`` is a loop variable rather than an argument of this - # maker, so ``_make_index`` has no argument default to infer the - # namespace of a :obj:`str` member name from; name it here rather than - # leave the name unresolvable (#2394). - if suite_namespace is None: - suite_namespace = Enum_ESPTransformSuite - - suite_id = [] - for suite in suites: - suite_id.append(self._make_index(suite, suite_default, namespace=suite_namespace, # type: ignore[arg-type] - reversed=suite_reversed, pack=False)) + suite_id = [self._make_index(suite, Enum_ESPTransformSuite, bits=16, name='suite ID', + alias=f'HIPv{version}') + for suite in suites or []] return Schema_ESPTransformParameter( reserved=reserved, @@ -5148,10 +4950,7 @@ def _make_param_ack_data(self, code: 'Enum_Parameter', param: 'Optional[Data_Ack def _make_param_payload_mic(self, code: 'Enum_Parameter', param: 'Optional[Data_PayloadMICParameter]' = None, *, # pylint: disable=unused-argument version: 'int', - next: 'Enum_TransType | StdlibEnum | AenumEnum | str | int' = Enum_TransType.UDP, - next_default: 'Optional[int]' = None, - next_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - next_reversed: 'bool' = False, + next: 'Enum_TransType | int' = Enum_TransType.UDP, payload: 'bytes' = b'', mic: 'bytes' = b'', reserved: 'bytes' = b'\x00\x00\x00', @@ -5163,9 +4962,6 @@ def _make_param_payload_mic(self, code: 'Enum_Parameter', param: 'Optional[Data_ param: parameter data version: HIP protocol version next: next protocol - next_default: default next protocol - next_namespace: next protocol namespace - next_reversed: reverse next protocol namespace payload: payload data mic: message integrity code reserved: the three reserved octets, truncated or @@ -5175,6 +4971,9 @@ def _make_param_payload_mic(self, code: 'Enum_Parameter', param: 'Optional[Data_ Returns: HIP parameter schema. + Raises: + ProtocolError: If ``next`` is a name or does not fit its field. + """ if param is not None: reserved = getattr(param, 'reserved', b'\x00\x00\x00') @@ -5182,8 +4981,8 @@ def _make_param_payload_mic(self, code: 'Enum_Parameter', param: 'Optional[Data_ payload = param.payload mic = param.mic else: - protocol = self._make_index(next, next_default, namespace=next_namespace, # type: ignore[assignment] - reversed=next_reversed, pack=False) + protocol = self._make_index(next, Enum_TransType, bits=8, name='next header', + alias=f'HIPv{version}') return Schema_PayloadMICParameter( reserved=reserved, @@ -5383,10 +5182,7 @@ def _make_param_route_dst(self, code: 'Enum_Parameter', param: 'Optional[Data_Ro def _make_param_hip_transport_mode(self, code: 'Enum_Parameter', param: 'Optional[Data_HIPTransportModeParameter]' = None, *, # pylint: disable=unused-argument version: 'int', port: 'int' = 0, - modes: 'Optional[list[Enum_Transport | StdlibEnum | AenumEnum | str | int]]' = None, - mode_default: 'Optional[int]' = None, - mode_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - mode_reversed: 'bool' = False, + modes: 'Optional[list[Enum_Transport | int]]' = None, **kwargs: 'Any') -> 'Schema_HIPTransportModeParameter': """Make HIP ``HIP_TRANSPORT_MODE`` parameter. @@ -5396,36 +5192,23 @@ def _make_param_hip_transport_mode(self, code: 'Enum_Parameter', param: 'Optiona version: HIP protocol version port: port number modes: transport mode list - mode_default: default transport mode - mode_namespace: transport mode namespace - mode_reversed: reverse transport mode namespace **kwargs: arbitrary keyword arguments Returns: HIP parameter schema. Raises: - ProtocolError: If ``port`` does not fit its field. + ProtocolError: If ``port`` or an entry of ``modes`` does not fit its + field, or an entry of ``modes`` is a name. """ if param is not None: port = param.port mode_id = cast('list[Enum_Transport]', param.mode_id) else: - if modes is None: - modes = [] - - # NOTE: ``mode`` is a loop variable rather than an argument of this - # maker, so ``_make_index`` has no argument default to infer the - # namespace of a :obj:`str` member name from; name it here rather than - # leave the name unresolvable (#2394). - if mode_namespace is None: - mode_namespace = Enum_Transport - - mode_id = [] - for mode in modes: - mode_id.append(self._make_index(mode, mode_default, namespace=mode_namespace, # type: ignore[arg-type] - reversed=mode_reversed, pack=False)) + mode_id = [self._make_index(mode, Enum_Transport, bits=16, name='mode ID', + alias=f'HIPv{version}') + for mode in modes or []] return Schema_HIPTransportModeParameter( type=code, @@ -5488,10 +5271,7 @@ def _make_param_hip_mac_2(self, code: 'Enum_Parameter', param: 'Optional[Data_HI def _make_param_hip_signature_2(self, code: 'Enum_Parameter', param: 'Optional[Data_HIPSignature2Parameter]' = None, *, # pylint: disable=unused-argument version: 'int', - algorithm: 'Enum_HIAlgorithm | StdlibEnum | AenumEnum | str | int' = Enum_HIAlgorithm.NULL_ENCRYPT, - algorithm_default: 'Optional[int]' = None, - algorithm_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - algorithm_reversed: 'bool' = False, + algorithm: 'Enum_HIAlgorithm | int' = Enum_HIAlgorithm.NULL_ENCRYPT, signature: 'bytes' = b'', **kwargs: 'Any') -> 'Schema_HIPSignature2Parameter': """Make HIP ``HIP_SIGNATURE_2`` parameter. @@ -5501,22 +5281,22 @@ def _make_param_hip_signature_2(self, code: 'Enum_Parameter', param: 'Optional[D param: parameter data version: HIP protocol version algorithm: signature algorithm - algorithm_default: default signature algorithm - algorithm_namespace: signature algorithm namespace - algorithm_reversed: reverse signature algorithm namespace signature: signature value **kwargs: arbitrary keyword arguments Returns: HIP parameter schema. + Raises: + ProtocolError: If ``algorithm`` is a name or does not fit its field. + """ if param is not None: algo = param.algorithm signature = param.signature else: - algo = self._make_index(algorithm, algorithm_default, namespace=algorithm_namespace, # type: ignore[assignment] - reversed=algorithm_reversed, pack=False) + algo = self._make_index(algorithm, Enum_HIAlgorithm, bits=16, + name='signature algorithm', alias=f'HIPv{version}') return Schema_HIPSignature2Parameter( type=code, @@ -5527,10 +5307,7 @@ def _make_param_hip_signature_2(self, code: 'Enum_Parameter', param: 'Optional[D def _make_param_hip_signature(self, code: 'Enum_Parameter', param: 'Optional[Data_HIPSignatureParameter]' = None, *, # pylint: disable=unused-argument version: 'int', - algorithm: 'Enum_HIAlgorithm | StdlibEnum | AenumEnum | str | int' = Enum_HIAlgorithm.NULL_ENCRYPT, - algorithm_default: 'Optional[int]' = None, - algorithm_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - algorithm_reversed: 'bool' = False, + algorithm: 'Enum_HIAlgorithm | int' = Enum_HIAlgorithm.NULL_ENCRYPT, signature: 'bytes' = b'', **kwargs: 'Any') -> 'Schema_HIPSignatureParameter': """Make HIP ``HIP_SIGNATURE`` parameter. @@ -5540,22 +5317,22 @@ def _make_param_hip_signature(self, code: 'Enum_Parameter', param: 'Optional[Dat param: parameter data version: HIP protocol version algorithm: signature algorithm - algorithm_default: default signature algorithm - algorithm_namespace: signature algorithm namespace - algorithm_reversed: reverse signature algorithm namespace signature: signature value **kwargs: arbitrary keyword arguments Returns: HIP parameter schema. + Raises: + ProtocolError: If ``algorithm`` is a name or does not fit its field. + """ if param is not None: algo = param.algorithm signature = param.signature else: - algo = self._make_index(algorithm, algorithm_default, namespace=algorithm_namespace, # type: ignore[assignment] - reversed=algorithm_reversed, pack=False) + algo = self._make_index(algorithm, Enum_HIAlgorithm, bits=16, + name='signature algorithm', alias=f'HIPv{version}') return Schema_HIPSignatureParameter( type=code, @@ -5619,10 +5396,7 @@ def _make_param_echo_response_unsigned(self, code: 'Enum_Parameter', param: 'Opt def _make_param_relay_from(self, code: 'Enum_Parameter', param: 'Optional[Data_RelayFromParameter]' = None, *, # pylint: disable=unused-argument version: 'int', port: 'int' = 0, - protocol: 'Enum_TransType | StdlibEnum | AenumEnum | str | int' = Enum_TransType.UDP, - protocol_default: 'Optional[int]' = None, - protocol_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - protocol_reversed: 'bool' = False, + protocol: 'Enum_TransType | int' = Enum_TransType.UDP, address: 'IPv6Address | str | int | bytes' = '::', reserved: 'bytes' = b'\x00', **kwargs: 'Any') -> 'Schema_RelayFromParameter': @@ -5634,9 +5408,6 @@ def _make_param_relay_from(self, code: 'Enum_Parameter', param: 'Optional[Data_R version: HIP protocol version port: port number protocol: transport protocol - protocol_default: default transport protocol - protocol_namespace: transport protocol namespace - protocol_reversed: reverse transport protocol namespace address: the address the relay server received the relayed packet from (:rfc:`5770`) reserved: the reserved octet, truncated or @@ -5647,7 +5418,8 @@ def _make_param_relay_from(self, code: 'Enum_Parameter', param: 'Optional[Data_R HIP parameter schema. Raises: - ProtocolError: If ``port`` does not fit its field. + ProtocolError: If ``port`` or ``protocol`` does not fit its field, + or ``protocol`` is a name. """ if param is not None: @@ -5656,8 +5428,8 @@ def _make_param_relay_from(self, code: 'Enum_Parameter', param: 'Optional[Data_R proto = param.protocol address = param.address else: - proto = self._make_index(protocol, protocol_default, namespace=protocol_namespace, # type: ignore[assignment] - reversed=protocol_reversed, pack=False) + proto = self._make_index(protocol, Enum_TransType, bits=8, name='protocol', + alias=f'HIPv{version}') return Schema_RelayFromParameter( reserved=reserved, @@ -5671,10 +5443,7 @@ def _make_param_relay_from(self, code: 'Enum_Parameter', param: 'Optional[Data_R def _make_param_relay_to(self, code: 'Enum_Parameter', param: 'Optional[Data_RelayToParameter]' = None, *, # pylint: disable=unused-argument version: 'int', port: 'int' = 0, - protocol: 'Enum_TransType | StdlibEnum | AenumEnum | str | int' = Enum_TransType.UDP, - protocol_default: 'Optional[int]' = None, - protocol_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - protocol_reversed: 'bool' = False, + protocol: 'Enum_TransType | int' = Enum_TransType.UDP, address: 'IPv6Address | str | int | bytes' = '::', reserved: 'bytes' = b'\x00', **kwargs: 'Any') -> 'Schema_RelayToParameter': @@ -5686,9 +5455,6 @@ def _make_param_relay_to(self, code: 'Enum_Parameter', param: 'Optional[Data_Rel version: HIP protocol version port: port number protocol: transport protocol - protocol_default: default transport protocol - protocol_namespace: transport protocol namespace - protocol_reversed: reverse transport protocol namespace address: the address the relay server forwards the packet to (:rfc:`5770`) reserved: the reserved octet, truncated or @@ -5699,7 +5465,8 @@ def _make_param_relay_to(self, code: 'Enum_Parameter', param: 'Optional[Data_Rel HIP parameter schema. Raises: - ProtocolError: If ``port`` does not fit its field. + ProtocolError: If ``port`` or ``protocol`` does not fit its field, + or ``protocol`` is a name. """ if param is not None: @@ -5708,8 +5475,8 @@ def _make_param_relay_to(self, code: 'Enum_Parameter', param: 'Optional[Data_Rel proto = param.protocol address = param.address else: - proto = self._make_index(protocol, protocol_default, namespace=protocol_namespace, # type: ignore[assignment] - reversed=protocol_reversed, pack=False) + proto = self._make_index(protocol, Enum_TransType, bits=8, name='protocol', + alias=f'HIPv{version}') return Schema_RelayToParameter( reserved=reserved, diff --git a/pcapkit/protocols/internet/hopopt.py b/pcapkit/protocols/internet/hopopt.py index cb80e3c32b..90cf1814f5 100644 --- a/pcapkit/protocols/internet/hopopt.py +++ b/pcapkit/protocols/internet/hopopt.py @@ -97,11 +97,9 @@ if TYPE_CHECKING: from datetime import timedelta - from enum import IntEnum as StdlibEnum from ipaddress import IPv4Address, IPv6Address - from typing import IO, Any, Callable, DefaultDict, NoReturn, Optional, Type + from typing import IO, Any, Callable, DefaultDict, NoReturn, Optional - from aenum import IntEnum as AenumEnum from mypy_extensions import DefaultArg, KwArg, NamedArg from typing_extensions import Literal @@ -223,6 +221,24 @@ class HOPOPT(IPv6_Ext[Data_HOPOPT, Schema_HOPOPT], * A PDM scale, given or derived from its delta, that does not fit its octet raises :exc:`~pcapkit.utilities.exceptions.ProtocolError`, where it warned and was written wrapped (:issue:`2614`). + * Each enumeration field of :meth:`make` and of the option makers, + including the type of each option given to :meth:`make`, takes a + member or an :class:`int` code, resolved with the enumeration's + ``get``; a name, a :obj:`bool` for a field wider than one bit, or a + code the field cannot carry raises + :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than + resolving against an enumeration inferred from the caller's frame, + writing a different value or raising another exception + (:issue:`2594`). + + .. versionremoved:: 1.6.0 + + The ``*_default``, ``*_namespace`` and ``*_reversed`` arguments that + went with each enumeration field of :meth:`make` and of the option + makers -- ``next``, ``alert``, ``mode``, ``tid_type``, ``func`` and + ``seed_type``; resolve a name with + :meth:`~pcapkit.protocols.protocol.Protocol.resolve_index` first + (:issue:`2594`). """ @@ -373,20 +389,14 @@ def read(self, length: 'Optional[int]' = None, *, extension: 'bool' = False, **k return self._decode_next_layer(hopopt, schema.next, length - hopopt.length) def make(self, - next: 'Enum_TransType | StdlibEnum | AenumEnum | str | int' = Enum_TransType.UDP, - next_default: 'Optional[int]' = None, - next_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - next_reversed: 'bool' = False, - options: 'Optional[list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes] | Option]' = None, # pylint: disable=line-too-long + next: 'Enum_TransType | int' = Enum_TransType.UDP, + options: 'Optional[list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes] | Option]' = None, # pylint: disable=line-too-long payload: 'bytes | ProtocolBase | Schema' = b'', **kwargs: 'Any') -> 'Schema_HOPOPT': """Make (construct) packet data. Args: next: Next header type. - next_default: Default value of next header type. - next_namespace: Namespace of next header type. - next_reversed: If the namespace of next header type is reversed. options: Hop-by-Hop Options. payload: Payload of current protocol. **kwargs: Arbitrary keyword arguments. @@ -395,12 +405,13 @@ def make(self, Constructed packet data. Raises: - ProtocolError: If the options make a header too long for its + ProtocolError: If ``next``, or the type of an option, is a name, a + :obj:`bool`, or a member or code that does not fit in one + octet; or if the options make a header too long for its one-octet ``Hdr Ext Len``. """ - next_value = self._make_index(next, next_default, namespace=next_namespace, - reversed=next_reversed, pack=False) + next_value = self._make_index(next, Enum_TransType, bits=8, name='next header') # NOTE: No options is not the same as no options area. The header is at # least 8 octets, 2 of them the fixed part, so the remaining 6 must be @@ -415,7 +426,7 @@ def make(self, length = self._make_uint((total_length - 6) // 8, bits=8, name='Hdr Ext Len') return Schema_HOPOPT( - next=next_value, # type: ignore[arg-type] + next=next_value, len=length, options=options_value, payload=payload, @@ -621,7 +632,7 @@ def _read_hopopt_options(self, length: 'int') -> 'Option': raise ProtocolError('HOPOPT: invalid format') return options - def _make_hopopt_options(self, options: 'list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes] | Option') -> 'tuple[list[Schema_Option | bytes], int]': + def _make_hopopt_options(self, options: 'list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes] | Option') -> 'tuple[list[Schema_Option | bytes], int]': # pylint: disable=line-too-long """Make options for HOPOPT. Args: @@ -665,7 +676,8 @@ def _make_hopopt_options(self, options: 'list[Schema_Option | tuple[Enum_Option, opt = schema opt_len = len(schema.pack()) else: - code, args = cast('tuple[Enum_Option, dict[str, Any]]', schema) + kind, args = cast('tuple[Enum_Option | int, dict[str, Any]]', schema) + code = self._make_index(kind, Enum_Option, bits=8, name='option type') if code in (Enum_Option.Pad1, Enum_Option.PadN): # ignore padding options by default continue @@ -698,7 +710,8 @@ def _make_hopopt_options(self, options: 'list[Schema_Option | tuple[Enum_Option, # included and in place, so that a parsed header rebuilds byte for # byte; only a header left short of alignment gets trailing padding. options_list = [] - for code, option in options.items(multi=True): + for key, option in options.items(multi=True): + code = self._make_index(key, Enum_Option, bits=8, name='option type') name = self._lookup_registry(self.__option__, code) if isinstance(name, str): meth_name = f'_make_opt_{name}' @@ -1618,10 +1631,7 @@ def _make_opt_tun(self, code: 'Enum_Option', opt: 'Optional[Data_TunnelEncapsula ) def _make_opt_ra(self, code: 'Enum_Option', opt: 'Optional[Data_RouterAlertOption]' = None, *, - alert: 'Enum_RouterAlert | StdlibEnum | AenumEnum | str | int' = Enum_RouterAlert.Datagram_contains_a_Multicast_Listener_Discovery_message, # pylint: disable=line-too-long - alert_default: 'Optional[int]' = None, - alert_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - alert_reversed: 'bool' = False, + alert: 'Enum_RouterAlert | int' = Enum_RouterAlert.Datagram_contains_a_Multicast_Listener_Discovery_message, # pylint: disable=line-too-long **kwargs: 'Any') -> 'Schema_RouterAlertOption': """Make HOPOPT router alert option. @@ -1629,26 +1639,18 @@ def _make_opt_ra(self, code: 'Enum_Option', opt: 'Optional[Data_RouterAlertOptio code: option type value opt: option data alert: router alert value - alert_default: default value of router alert - alert_namespace: namespace of router alert - alert_reversed: reversed flag of router alert **kwargs: arbitrary keyword arguments Returns: Constructed option schema. Raises: - ProtocolError: If the router alert value does not fit in two - octets. + ProtocolError: If the router alert value is a name, a :obj:`bool`, + or a member or code that does not fit in two octets. """ - if opt is not None: - value = opt.value - else: - value = self._make_index(alert, alert_default, namespace=alert_namespace, # type: ignore[assignment] - reversed=alert_reversed, pack=False) - - self._make_uint(value, bits=16, name='router alert value') + value = self._make_index(opt.value if opt is not None else alert, Enum_RouterAlert, + bits=16, name='router alert value') return Schema_RouterAlertOption( type=code, len=2, @@ -1737,14 +1739,8 @@ def _make_opt_calipso(self, code: 'Enum_Option', opt: 'Optional[Data_CALIPSOOpti ) def _make_opt_smf_dpd(self, code: 'Enum_Option', opt: 'Optional[Data_SMFIdentificationBasedDPDOption | Data_SMFHashBasedDPDOption]' = None, *, - mode: 'Enum_SMFDPDMode | StdlibEnum | AenumEnum | str | int' = Enum_SMFDPDMode.I_DPD, - mode_default: 'Optional[int]' = None, - mode_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - mode_reversed: 'bool' = False, - tid_type: 'Optional[Enum_TaggerID | StdlibEnum | AenumEnum | str | int]' = None, - tid_type_default: 'Optional[int]' = None, - tid_type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - tid_type_reversed: 'bool' = False, + mode: 'Enum_SMFDPDMode | int' = Enum_SMFDPDMode.I_DPD, + tid_type: 'Optional[Enum_TaggerID | int]' = None, tid: 'Optional[bytes | IPv4Address | IPv6Address]' = None, id: 'bytes' = b'', hav: 'bytes' = b'', @@ -1755,14 +1751,8 @@ def _make_opt_smf_dpd(self, code: 'Enum_Option', opt: 'Optional[Data_SMFIdentifi code: option type value opt: option data mode: DPD mode - mode_default: default value of DPD mode - mode_namespace: namespace of DPD mode - mode_reversed: reversed flag of DPD mode tid_type: TaggerID type (``TidTy`` field); inferred from the length of ``tid`` if not given - tid_type_default: default value of TaggerID type - tid_type_namespace: namespace of TaggerID type - tid_type_reversed: reversed flag of TaggerID type tid: Tagger ID id: identifier hav: hash assist value (HAV) @@ -1772,10 +1762,12 @@ def _make_opt_smf_dpd(self, code: 'Enum_Option', opt: 'Optional[Data_SMFIdentifi Constructed option schema. Raises: - ProtocolError: If the DPD mode is invalid, the Tagger ID does - not fit the TaggerID type, the H-DPD Hash Assist Value is - empty, or the option data is too long for its one-octet - ``Opt Data Len``. + ProtocolError: If the DPD mode is a name, or a member or code + that does not fit in one bit; if the TaggerID type is a name, + a :obj:`bool`, or a member or code that does not fit in three + bits; if the Tagger ID does not fit the TaggerID type, the + H-DPD Hash Assist Value is empty, or the option data is too + long for its one-octet ``Opt Data Len``. """ if opt is not None: @@ -1785,8 +1777,7 @@ def _make_opt_smf_dpd(self, code: 'Enum_Option', opt: 'Optional[Data_SMFIdentifi id = getattr(opt, 'id', b'') hav = getattr(opt, 'hav', b'') - dpd_type = self._make_index(mode, mode_default, namespace=mode_namespace, - reversed=mode_reversed, pack=False) + dpd_type = self._make_index(mode, Enum_SMFDPDMode, bits=1, name='DPD mode') if dpd_type == Enum_SMFDPDMode.I_DPD: if tid is None: @@ -1800,13 +1791,8 @@ def _make_opt_smf_dpd(self, code: 'Enum_Option', opt: 'Optional[Data_SMFIdentifi tid_len = len(tid_bytes) if tid_type is not None: - # NOTE: ``tid_type`` defaults to :data:`None`, so ``_make_index`` has no - # enum default to infer the namespace of a :obj:`str` member name from; - # name it here rather than leave the name unresolvable (#2356). - tid_kind = self._make_index( - tid_type, tid_type_default, - namespace=tid_type_namespace if tid_type_namespace is not None else Enum_TaggerID, - reversed=tid_type_reversed, pack=False) + tid_kind = self._make_index(tid_type, Enum_TaggerID, bits=3, + name='TaggerID type') # type: Enum_TaggerID | int elif tid_len == 0: tid_kind = Enum_TaggerID.NULL elif tid_len == 4: @@ -1840,8 +1826,9 @@ def _make_opt_smf_dpd(self, code: 'Enum_Option', opt: 'Optional[Data_SMFIdentifi tid=tid_bytes if tid_len else None, id=id, ) # type: Schema_SMFDPDOption - elif dpd_type == Enum_SMFDPDMode.H_DPD: - # The first octet of the HAV carries the H-DPD mode bit. + else: + # H-DPD, the other value of the one-bit mode. The first octet of + # the HAV carries the H-DPD mode bit. if not hav: raise ProtocolError(f'{self.alias}: [OptNo {code}] empty Hash Assist Value (HAV)') hav_ba = bytearray(hav) @@ -1852,8 +1839,6 @@ def _make_opt_smf_dpd(self, code: 'Enum_Option', opt: 'Optional[Data_SMFIdentifi len=self._make_uint(len(hav), bits=8, name='Opt Data Len'), hav=bytes(hav_ba), ) - else: - raise ProtocolError(f'{self.alias}: [OptNo {code}] invalid DPD type: {dpd_type}') return schema def _make_opt_pdm(self, code: 'Enum_Option', opt: 'Optional[Data_PDMOption]' = None, *, @@ -1924,10 +1909,7 @@ def _make_opt_pdm(self, code: 'Enum_Option', opt: 'Optional[Data_PDMOption]' = N ) def _make_opt_qs(self, code: 'Enum_Option', opt: 'Optional[Data_QuickStartOption | Data_UnassignedOption]' = None, *, # pylint: disable=line-too-long - func: 'Enum_QSFunction | StdlibEnum | AenumEnum | str | int' = Enum_QSFunction.Quick_Start_Request, - func_default: 'Optional[int]' = None, - func_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - func_reversed: 'bool' = False, + func: 'Enum_QSFunction | int' = Enum_QSFunction.Quick_Start_Request, rate: 'int' = 0, ttl: 'timedelta | int' = 0, nonce: 'int' = 0, @@ -1940,9 +1922,6 @@ def _make_opt_qs(self, code: 'Enum_Option', opt: 'Optional[Data_QuickStartOption code: option type value opt: option data func: QS function type - func_default: default value for QS function type - func_namespace: namespace for QS function type - func_reversed: reversed flag for QS function type rate: rate (in kbps), rounded down to the nearest rate the four-bit rate code carries (:rfc:`4782#section-3.1`) ttl: time to live (in seconds) @@ -1958,9 +1937,10 @@ def _make_opt_qs(self, code: 'Enum_Option', opt: 'Optional[Data_QuickStartOption the option data takes the layout of a Quick-Start Request. Raises: - ProtocolError: If ``func`` does not fit in four bits, the rate code - cannot carry ``rate``, or ``ttl``, ``nonce`` or ``reserved`` does - not fit its field. + ProtocolError: If ``func`` is a name, a :obj:`bool`, or a member or + code that does not fit in four bits, the rate code cannot carry + ``rate``, or ``ttl``, ``nonce`` or ``reserved`` does not fit its + field. """ if isinstance(opt, Data_UnassignedOption): @@ -1973,8 +1953,7 @@ def _make_opt_qs(self, code: 'Enum_Option', opt: 'Optional[Data_QuickStartOption reserved = getattr(opt, 'reserved', 0) unused = getattr(opt, 'unused', b'\x00') else: - func_enum = self._make_index(func, func_default, namespace=func_namespace, # type: ignore[assignment] - reversed=func_reversed, pack=False) + func_enum = self._make_index(func, Enum_QSFunction, bits=4, name='QS function') rate_val = self._make_qs_rate(rate) ttl_value = ttl if isinstance(ttl, int) else math.floor(ttl.total_seconds()) nonce = self._make_uint(nonce, bits=30, name='Quick-Start nonce') @@ -2074,10 +2053,7 @@ def _make_opt_rpl(self, code: 'Enum_Option', opt: 'Optional[Data_RPLOption]' = N ) def _make_opt_mpl(self, code: 'Enum_Option', opt: 'Optional[Data_MPLOption]' = None, *, - seed_type: 'Optional[Enum_SeedID | StdlibEnum | AenumEnum | str | int]' = None, - seed_type_default: 'Optional[int]' = None, - seed_type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - seed_type_reversed: 'bool' = False, + seed_type: 'Optional[Enum_SeedID | int]' = None, max: 'bool' = False, drop: 'bool' = False, reserved: 'int' = 0, @@ -2090,9 +2066,6 @@ def _make_opt_mpl(self, code: 'Enum_Option', opt: 'Optional[Data_MPLOption]' = N code: option type value opt: option data seed_type: seed-id type (``S`` field); inferred from ``seed`` if not given - seed_type_default: default value of seed-id type - seed_type_namespace: namespace of seed-id type - seed_type_reversed: reversed flag of seed-id type max: maximum sequence number flag drop: drop packet flag reserved: reserved bits (``rsv``), i.e. the low four bits of the @@ -2107,8 +2080,10 @@ def _make_opt_mpl(self, code: 'Enum_Option', opt: 'Optional[Data_MPLOption]' = N Constructed option schema. Raises: - ProtocolError: If the seed ID is missing or does not fit the seed-id - type, or ``reserved`` or ``seq`` does not fit its field. + ProtocolError: If the seed-id type is a name, a :obj:`bool`, or a + member or code that does not fit in two bits; if the seed ID is + missing or does not fit the seed-id type, or ``reserved`` or + ``seq`` does not fit its field. """ if opt is not None: @@ -2123,13 +2098,8 @@ def _make_opt_mpl(self, code: 'Enum_Option', opt: 'Optional[Data_MPLOption]' = N seq = self._make_uint(seq, bits=8, name='MPL sequence number') if seed_type is not None: - # NOTE: ``seed_type`` defaults to :data:`None`, so ``_make_index`` has no - # enum default to infer the namespace of a :obj:`str` member name from; - # name it here rather than leave the name unresolvable (#2356). - kind = self._make_index( - seed_type, seed_type_default, - namespace=seed_type_namespace if seed_type_namespace is not None else Enum_SeedID, - reversed=seed_type_reversed, pack=False) + kind = self._make_index(seed_type, Enum_SeedID, bits=2, + name='MPL seed-id type') # type: Enum_SeedID | int elif seed is None or isinstance(seed, ipaddress.IPv6Address): kind = Enum_SeedID.IPV6_SOURCE_ADDRESS else: @@ -2150,10 +2120,8 @@ def _make_opt_mpl(self, code: 'Enum_Option', opt: 'Optional[Data_MPLOption]' = N clen = 4 elif kind == Enum_SeedID.SEEDID_64_BIT_UNSIGNED_INTEGER: clen = 10 - elif kind == Enum_SeedID.SEEDID_128_BIT_UNSIGNED_INTEGER: + else: # the 128-bit seed, the last value of the two-bit type clen = 18 - else: - raise ProtocolError(f'{self.alias}: [OptNo {code}] invalid MPL seed-id type: {kind}') if seed is None: raise ProtocolError(f'{self.alias}: [OptNo {code}] missing MPL seed ID for type {kind}') diff --git a/pcapkit/protocols/internet/icmpv4.py b/pcapkit/protocols/internet/icmpv4.py index 79f247b8bf..19fa4e2ff7 100644 --- a/pcapkit/protocols/internet/icmpv4.py +++ b/pcapkit/protocols/internet/icmpv4.py @@ -50,10 +50,8 @@ from pcapkit.utilities.warnings import RegistryWarning, warn if TYPE_CHECKING: - from enum import IntEnum as StdlibEnum - from typing import Any, Callable, DefaultDict, NoReturn, Optional, Type + from typing import Any, Callable, DefaultDict, NoReturn, Optional - from aenum import IntEnum as AenumEnum from mypy_extensions import DefaultArg, KwArg, NamedArg from typing_extensions import Literal @@ -216,10 +214,7 @@ def read(self, length: 'Optional[int]' = None, *, return icmp def make(self, - type: 'Enum_Type | StdlibEnum | AenumEnum | str | int' = Enum_Type.Echo, # pylint: disable=redefined-builtin - type_default: 'Optional[int]' = None, - type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - type_reversed: 'bool' = False, + type: 'Enum_Type | int' = Enum_Type.Echo, # pylint: disable=redefined-builtin code: 'int' = 0, checksum: 'Optional[bytes]' = None, # Echo identifier and sequence number, the shortest body. @@ -229,9 +224,6 @@ def make(self, Args: type: ICMP type. - type_default: Default value of ICMP type. - type_namespace: Namespace of ICMP type. - type_reversed: If the namespace of ICMP type is reversed. code: ICMP code; meaning is entirely ``type``-dependent. checksum: Checksum; if :data:`None`, computed over the message (:rfc:`792`) when :data:`~pcapkit.utilities.config.CHECKSUM_COMPUTE` @@ -248,14 +240,13 @@ def make(self, Constructed packet data. Raises: - ProtocolError: If ``body`` is :obj:`bytes` that do not parse as - the message body of ``type``, or is of none of the accepted - types. + ProtocolError: If ``type`` is a name, a :obj:`bool`, or a member + or code that does not fit in one octet; or if ``body`` is + :obj:`bytes` that do not parse as the message body of + ``type``, or is of none of the accepted types. """ - type_val = cast('Enum_Type', - self._make_index(type, type_default, namespace=type_namespace, - reversed=type_reversed, pack=False)) + type_val = self._make_index(type, Enum_Type, bits=8, name='type') if isinstance(body, bytes): try: diff --git a/pcapkit/protocols/internet/icmpv6.py b/pcapkit/protocols/internet/icmpv6.py index 7aff29c94b..03829b8a2a 100644 --- a/pcapkit/protocols/internet/icmpv6.py +++ b/pcapkit/protocols/internet/icmpv6.py @@ -52,10 +52,8 @@ from pcapkit.utilities.warnings import RegistryWarning, warn if TYPE_CHECKING: - from enum import IntEnum as StdlibEnum - from typing import Any, Callable, DefaultDict, NoReturn, Optional, Type + from typing import Any, Callable, DefaultDict, NoReturn, Optional - from aenum import IntEnum as AenumEnum from mypy_extensions import DefaultArg, KwArg, NamedArg from typing_extensions import Literal @@ -216,10 +214,7 @@ def read(self, length: 'Optional[int]' = None, *, return icmpv6 def make(self, - type: 'Enum_Type | StdlibEnum | AenumEnum | str | int' = Enum_Type.Echo_Request, # pylint: disable=redefined-builtin - type_default: 'Optional[int]' = None, - type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - type_reversed: 'bool' = False, + type: 'Enum_Type | int' = Enum_Type.Echo_Request, # pylint: disable=redefined-builtin code: 'int' = 0, checksum: 'Optional[bytes]' = None, # Echo identifier and sequence number, the shortest body. @@ -230,9 +225,6 @@ def make(self, Args: type: ICMPv6 type. - type_default: Default value of ICMPv6 type. - type_namespace: Namespace of ICMPv6 type. - type_reversed: If the namespace of ICMPv6 type is reversed. code: ICMPv6 code; meaning is entirely ``type``-dependent. checksum: Checksum; if :data:`None` and :data:`~pcapkit.utilities.config.CHECKSUM_COMPUTE` is on, @@ -253,14 +245,13 @@ def make(self, Constructed packet data. Raises: - ProtocolError: If ``body`` is :obj:`bytes` that do not parse as - the message body of ``type``, or is of none of the accepted - types. + ProtocolError: If ``type`` is a name, a :obj:`bool`, or a member + or code that does not fit in one octet; or if ``body`` is + :obj:`bytes` that do not parse as the message body of + ``type``, or is of none of the accepted types. """ - type_val = cast('Enum_Type', - self._make_index(type, type_default, namespace=type_namespace, - reversed=type_reversed, pack=False)) + type_val = self._make_index(type, Enum_Type, bits=8, name='type') if isinstance(body, bytes): try: diff --git a/pcapkit/protocols/internet/igmp.py b/pcapkit/protocols/internet/igmp.py index 52a958e8f2..0fdd0e2521 100644 --- a/pcapkit/protocols/internet/igmp.py +++ b/pcapkit/protocols/internet/igmp.py @@ -39,11 +39,9 @@ from pcapkit.utilities.exceptions import ProtocolError if TYPE_CHECKING: - from enum import IntEnum as StdlibEnum from ipaddress import IPv4Address from typing import IO, Any, Literal, Optional, Type - from aenum import IntEnum as AenumEnum from typing_extensions import Self from pcapkit.protocols.protocol import ProtocolBase @@ -159,10 +157,7 @@ def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> '_PT': return self._decode_next_layer(igmp, None, length - self.length) def make(self, - type: 'Enum_Type | StdlibEnum | AenumEnum | str | int' = Enum_Type.IGMP_Membership_Query, - type_default: 'Optional[int]' = None, - type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - type_reversed: 'bool' = False, + type: 'Enum_Type | int' = Enum_Type.IGMP_Membership_Query, max_resp_time: 'int' = 0, checksum: 'Optional[bytes]' = None, group: 'IPv4Address | str | bytes | int' = '0.0.0.0', # nosec: B104 @@ -176,9 +171,6 @@ def make(self, Args: type: Message type. - type_default: Default value of message type. - type_namespace: Namespace of message type. - type_reversed: If the namespace is reversed. max_resp_time: Max response time, in tenths of a second; zero for an IGMPv1 message. checksum: Checksum over the message with this field zeroed, no @@ -194,18 +186,16 @@ def make(self, Constructed packet data. Raises: - ProtocolError: If ``type`` is outside 0 to 255, or ``max_resp_time`` - is not a non-:obj:`bool` integer from 0 to 255. + ProtocolError: If ``type`` is a name, a :obj:`bool`, or a member + or code outside 0 to 255, or ``max_resp_time`` is not a + non-:obj:`bool` integer from 0 to 255. """ - type_value = self._make_index(type, type_default, namespace=type_namespace, - reversed=type_reversed, pack=False) - for name, value in (('type', type_value), ('max_resp_time', max_resp_time)): - if isinstance(value, bool) or not isinstance(value, int) or not 0 <= value <= 255: - raise ProtocolError(f'{self.alias}: invalid {name}: {value!r}') + type_value = self._make_index(type, Enum_Type, bits=8, name='type') + max_resp_time = self._make_uint(max_resp_time, bits=8, name='max_resp_time') schema = Schema_IGMP( - type=type_value, # type: ignore[arg-type] + type=type_value, max_resp_time=max_resp_time, checksum=b'\x00\x00' if checksum is None else checksum, group=group, diff --git a/pcapkit/protocols/internet/igmpv3.py b/pcapkit/protocols/internet/igmpv3.py index 466c148f39..b9ac2a37ef 100644 --- a/pcapkit/protocols/internet/igmpv3.py +++ b/pcapkit/protocols/internet/igmpv3.py @@ -68,11 +68,9 @@ from pcapkit.utilities.exceptions import ProtocolError if TYPE_CHECKING: - from enum import IntEnum as StdlibEnum from ipaddress import IPv4Address - from typing import Any, Iterable, Optional, Type + from typing import Any, Iterable, Optional - from aenum import IntEnum as AenumEnum from typing_extensions import Literal from pcapkit.protocols.protocol import ProtocolBase @@ -309,10 +307,7 @@ def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_IGMPv3' return self._decode_next_layer(igmp, None, len(trailing)) def make(self, - type: 'Enum_Type | StdlibEnum | AenumEnum | str | int' = Enum_Type.IGMP_Membership_Query, # pylint: disable=redefined-builtin - type_default: 'Optional[int]' = None, - type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - type_reversed: 'bool' = False, + type: 'Enum_Type | int' = Enum_Type.IGMP_Membership_Query, # pylint: disable=redefined-builtin max_resp_code: 'Optional[int]' = None, max_resp_time: 'int' = 0, checksum: 'Optional[bytes]' = None, @@ -339,9 +334,6 @@ def make(self, Args: type: Message type. - type_default: Default value of message type. - type_namespace: Namespace of message type. - type_reversed: If the namespace is reversed. max_resp_code: Max response code, as transmitted on the wire. When not given, derived from ``max_resp_time`` via :func:`_encode_resp_code`; only meaningful for a Query. @@ -392,14 +384,15 @@ def make(self, Constructed packet data. Raises: - ProtocolError: If ``max_resp_time`` is not an :obj:`int` from 0 - to 255, ``num_group_records`` is not an :obj:`int` from the - length of ``records`` to 65535, or ``type`` is neither a Query - nor a Report. + ProtocolError: If ``type`` is a name, a :obj:`bool`, or a member + or code outside 0 to 255, or is neither a Query nor a Report; + if ``max_resp_time`` is not an :obj:`int` from 0 to 255; if + ``num_group_records`` is not an :obj:`int` from the length of + ``records`` to 65535; or if a record's ``record_type`` is + refused, see :meth:`_make_group_record`. """ - type_value = self._make_index(type, type_default, namespace=type_namespace, - reversed=type_reversed, pack=False) + type_value = self._make_index(type, Enum_Type, bits=8, name='type') if isinstance(max_resp_time, bool) or not isinstance(max_resp_time, int) \ or not 0 <= max_resp_time <= 255: raise ProtocolError(f'{self.alias}: invalid max_resp_time: {max_resp_time!r}') @@ -513,8 +506,7 @@ def _make_data(cls, data: 'Data_IGMPv3') -> 'dict[str, Any]': # type: ignore[ov 'payload': cls._make_payload(data), } - @staticmethod - def _make_group_record(record: 'Optional[Data_GroupRecord]' = None, *, + def _make_group_record(self, record: 'Optional[Data_GroupRecord]' = None, *, record_type: 'Enum_RecordType | int' = Enum_RecordType.MODE_IS_INCLUDE, aux_data_len: 'Optional[int]' = None, num_sources: 'Optional[int]' = None, @@ -544,6 +536,10 @@ def _make_group_record(record: 'Optional[Data_GroupRecord]' = None, *, Returns: Group record schema. + Raises: + ProtocolError: If ``record_type`` is a name, a :obj:`bool`, or a + member or code outside 0 to 255. + """ if record is not None: return Schema_GroupRecord( @@ -557,7 +553,7 @@ def _make_group_record(record: 'Optional[Data_GroupRecord]' = None, *, source_list = [] if sources is None else list(sources) return Schema_GroupRecord( - record_type=record_type, + record_type=self._make_index(record_type, Enum_RecordType, bits=8, name='record type'), aux_data_len=len(aux_data) // 4 if aux_data_len is None else aux_data_len, num_sources=len(source_list) if num_sources is None else num_sources, multicast_address=multicast_address, diff --git a/pcapkit/protocols/internet/ipv4.py b/pcapkit/protocols/internet/ipv4.py index cd65390fd6..e0ddc3b531 100644 --- a/pcapkit/protocols/internet/ipv4.py +++ b/pcapkit/protocols/internet/ipv4.py @@ -112,11 +112,9 @@ if TYPE_CHECKING: from datetime import timedelta - from enum import IntEnum as StdlibEnum from ipaddress import IPv4Address - from typing import IO, Any, Callable, DefaultDict, Optional, Type + from typing import IO, Any, Callable, DefaultDict, Optional - from aenum import IntEnum as AenumEnum from mypy_extensions import DefaultArg, KwArg, NamedArg from typing_extensions import Literal @@ -229,9 +227,27 @@ class IPv4(IP[Data_IPv4, Schema_IPv4], :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than writing a different value or raising another exception; so do the route and timestamp makers for a slot count that is a :obj:`bool`, - and :meth:`make` for options too long for the IHL. The registry - codes :meth:`make` takes, such as ``protocol``, are not - range-checked (:issue:`2614`). + and :meth:`make` for options too long for the IHL (:issue:`2614`). + * Each enumeration field of :meth:`make` and of the security, router + alert and Quick-Start option makers, and the type of each option + given to :meth:`make`, takes a member or an :class:`int` code, + resolved with the enumeration's ``get``; a name, a :obj:`bool` for a + field wider than one bit, or a code the field cannot carry raises + :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than + resolving against an enumeration inferred from the caller's frame, + writing a different value or raising a bare + :exc:`~pcapkit.utilities.exceptions.FieldValueError` + (:issue:`2594`). + + .. versionremoved:: 1.6.0 + + The ``_default``, ``_namespace`` and + ``_reversed`` arguments that went with each enumeration field + of :meth:`make` (``tos_pre``, ``tos_del``, ``tos_thr``, ``tos_rel``, + ``tos_ecn`` and ``protocol``) and of the security, router alert and + Quick-Start option makers (``level``, ``alert`` and ``func``); resolve + a name with :meth:`~pcapkit.protocols.protocol.Protocol.resolve_index` + first (:issue:`2594`). """ @@ -454,40 +470,22 @@ def read(self, length: 'Optional[int]' = None, *, # pylint: disable=unused-argu return self._decode_next_layer(ipv4, proto, _plen, packet=__packet__, declared=declared) def make(self, - tos_pre: 'Enum_ToSPrecedence | StdlibEnum | AenumEnum | int | str' = Enum_ToSPrecedence.Routine, - tos_pre_default: 'Optional[int]' = None, - tos_pre_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - tos_pre_reversed: 'bool' = False, - tos_del: 'Enum_ToSDelay | StdlibEnum | AenumEnum | int | str' = Enum_ToSDelay.NORMAL, - tos_del_default: 'Optional[int]' = None, - tos_del_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - tos_del_reversed: 'bool' = False, - tos_thr: 'Enum_ToSThroughput | StdlibEnum | AenumEnum | int | str' = Enum_ToSThroughput.NORMAL, - tos_thr_default: 'Optional[int]' = None, - tos_thr_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - tos_thr_reversed: 'bool' = False, - tos_rel: 'Enum_ToSReliability | StdlibEnum | AenumEnum | int | str' = Enum_ToSReliability.NORMAL, - tos_rel_default: 'Optional[int]' = None, - tos_rel_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - tos_rel_reversed: 'bool' = False, - tos_ecn: 'Enum_ToSECN | StdlibEnum | AenumEnum | int | str' = Enum_ToSECN.Not_ECT, - tos_ecn_default: 'Optional[int]' = None, - tos_ecn_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - tos_ecn_reversed: 'bool' = False, + tos_pre: 'Enum_ToSPrecedence | int' = Enum_ToSPrecedence.Routine, + tos_del: 'Enum_ToSDelay | int' = Enum_ToSDelay.NORMAL, + tos_thr: 'Enum_ToSThroughput | int' = Enum_ToSThroughput.NORMAL, + tos_rel: 'Enum_ToSReliability | int' = Enum_ToSReliability.NORMAL, + tos_ecn: 'Enum_ToSECN | int' = Enum_ToSECN.Not_ECT, id: 'int' = 0, reserved: 'int' = 0, df: 'bool' = False, mf: 'bool' = False, offset: 'int' = 0, ttl: 'timedelta | int' = 0, - protocol: 'Enum_TransType | StdlibEnum | AenumEnum | int | str' = Enum_TransType.UDP, - protocol_default: 'Optional[int]' = None, - protocol_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - protocol_reversed: 'bool' = False, + protocol: 'Enum_TransType | int' = Enum_TransType.UDP, checksum: 'Optional[bytes]' = None, src: 'IPv4Address | str | int | bytes' = '127.0.0.1', dst: 'IPv4Address | str | int | bytes' = '0.0.0.0', # nosec: B104 - options: 'Optional[list[Schema_Option | tuple[Enum_OptionNumber, dict[str, Any]] | bytes] | Option]' = None, # pylint: disable=line-too-long + options: 'Optional[list[Schema_Option | tuple[Enum_OptionNumber | int, dict[str, Any]] | bytes] | Option]' = None, # pylint: disable=line-too-long total_length: 'Optional[int]' = None, padding: 'Optional[bytes]' = None, payload: 'bytes | ProtocolBase | Schema' = b'', @@ -497,25 +495,10 @@ def make(self, Args: tos_pre: Precedence of the packet. - tos_pre_default: Default value of ``tos_pre``. - tos_pre_namespace: Namespace of ``tos_pre``. - tos_pre_reversed: If the namespace of ``tos_pre`` is reversed. tos_del: Delay of the packet. - tos_del_default: Default value of ``tos_del``. - tos_del_namespace: Namespace of ``tos_del``. - tos_del_reversed: If the namespace of ``tos_del`` is reversed. tos_thr: Throughput of the packet. - tos_thr_default: Default value of ``tos_thr``. - tos_thr_namespace: Namespace of ``tos_thr``. - tos_thr_reversed: If the namespace of ``tos_thr`` is reversed. tos_rel: Reliability of the packet. - tos_rel_default: Default value of ``tos_rel``. - tos_rel_namespace: Namespace of ``tos_rel``. - tos_rel_reversed: If the namespace of ``tos_rel`` is reversed. tos_ecn: ECN of the packet. - tos_ecn_default: Default value of ``tos_ecn``. - tos_ecn_namespace: Namespace of ``tos_ecn``. - tos_ecn_reversed: If the namespace of ``tos_ecn`` is reversed. id: Identification of the packet. reserved: Reserved flag bit, i.e. the high-order bit of the flags. df: Don't fragment flag. @@ -524,9 +507,6 @@ def make(self, the parsed ``offset`` is in octets, see :meth:`_make_data`. ttl: Time to live of the packet. protocol: Payload protocol of the packet. - protocol_default: Default value of ``protocol``. - protocol_namespace: Namespace of ``protocol``. - protocol_reversed: If the namespace of ``protocol`` is reversed. checksum: Header checksum; computed over the header (:rfc:`791`) if :data:`None` and :data:`~pcapkit.utilities.config.CHECKSUM_COMPUTE` is on, zero @@ -549,8 +529,11 @@ def make(self, Raises: ProtocolError: If ``id``, ``reserved``, ``offset``, ``ttl`` or the - total length does not fit its field, or the options make a - header too long for its four-bit IHL. + total length does not fit its field; if a ToS field, + ``protocol`` or the type of an option is a name, a :obj:`bool` + for a field wider than one bit, or a member or code its field + cannot carry; or if the options make a header too long for its + four-bit IHL. .. versionchanged:: 1.5.0 @@ -559,19 +542,13 @@ def make(self, ahead of ``payload``, which shifts every later positional argument. """ - tos_pre_val = self._make_index(tos_pre, tos_pre_default, namespace=tos_pre_namespace, - reversed=tos_pre_reversed, pack=False) - tos_del_val = self._make_index(tos_del, tos_del_default, namespace=tos_del_namespace, - reversed=tos_del_reversed, pack=False) - tos_thr_val = self._make_index(tos_thr, tos_thr_default, namespace=tos_thr_namespace, - reversed=tos_thr_reversed, pack=False) - tos_rel_val = self._make_index(tos_rel, tos_rel_default, namespace=tos_rel_namespace, - reversed=tos_rel_reversed, pack=False) - tos_ecn_val = self._make_index(tos_ecn, tos_ecn_default, namespace=tos_ecn_namespace, - reversed=tos_ecn_reversed, pack=False) - - proto = self._make_index(protocol, protocol_default, namespace=protocol_namespace, - reversed=protocol_reversed, pack=False) + tos_pre_val = self._make_index(tos_pre, Enum_ToSPrecedence, bits=3, name='precedence') + tos_del_val = self._make_index(tos_del, Enum_ToSDelay, bits=1, name='delay') + tos_thr_val = self._make_index(tos_thr, Enum_ToSThroughput, bits=1, name='throughput') + tos_rel_val = self._make_index(tos_rel, Enum_ToSReliability, bits=1, name='reliability') + tos_ecn_val = self._make_index(tos_ecn, Enum_ToSECN, bits=2, name='ECN') + + proto = self._make_index(protocol, Enum_TransType, bits=8, name='protocol') ttl_val = self._make_uint(ttl if isinstance(ttl, int) else math.ceil(ttl.total_seconds()), bits=8, name='TTL') id = self._make_uint(id, bits=16, name='identification') @@ -615,7 +592,7 @@ def make(self, 'offset': offset, }, ttl=ttl_val, - proto=proto, # type: ignore[arg-type] + proto=proto, chksum=b'\x00\x00' if checksum is None else checksum, src=src, dst=dst, @@ -874,7 +851,7 @@ def _read_ipv4_options(self, length: 'int') -> 'Option': raise ProtocolError(f'IPv4: invalid format') return options - def _make_ipv4_options(self, options: 'list[Schema_Option | tuple[Enum_OptionNumber, dict[str, Any]] | bytes] | Option', + def _make_ipv4_options(self, options: 'list[Schema_Option | tuple[Enum_OptionNumber | int, dict[str, Any]] | bytes] | Option', # pylint: disable=line-too-long padding: 'Optional[bytes]' = None) -> 'tuple[list[Schema_Option | bytes], int]': """Make options for IPv4. @@ -897,6 +874,11 @@ def _make_ipv4_options(self, options: 'list[Schema_Option | tuple[Enum_OptionNum Returns: Tuple of options and total length of options. + Raises: + ProtocolError: If the type of an option, given in a ``(type, + kwargs)`` pair or as the key of a container, is a name, a + :obj:`bool` or a code its field cannot carry. + """ total_length = 0 terminated = False @@ -918,7 +900,8 @@ def _make_ipv4_options(self, options: 'list[Schema_Option | tuple[Enum_OptionNum data = schema data_len = len(schema.pack()) else: - code, args = cast('tuple[Enum_OptionNumber, dict[str, Any]]', schema) + kind, args = cast('tuple[Enum_OptionNumber | int, dict[str, Any]]', schema) + code = self._make_index(kind, Enum_OptionNumber, bits=8, name='option type') if code in (Enum_OptionNumber.NOP, Enum_OptionNumber.EOOL): # ignore padding options by default continue @@ -936,7 +919,8 @@ def _make_ipv4_options(self, options: 'list[Schema_Option | tuple[Enum_OptionNum options_list.append(data) total_length += data_len else: - for code, option in options.items(multi=True): + for key, option in options.items(multi=True): + code = self._make_index(key, Enum_OptionNumber, bits=8, name='option type') name = self._lookup_registry(self.__option__, code) if isinstance(name, str): meth_name = f'_make_opt_{name}' @@ -1659,10 +1643,7 @@ def _make_opt_nop(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_NOPOpt ) def _make_opt_sec(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_SECOption]' = None, *, - level: 'Enum_ClassificationLevel | StdlibEnum | AenumEnum | int | str' = Enum_ClassificationLevel.Unclassified, - level_default: 'Optional[int]' = None, - level_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - level_reversed: 'bool' = False, + level: 'Enum_ClassificationLevel | int' = Enum_ClassificationLevel.Unclassified, authorities: 'Optional[list[Enum_ProtectionAuthority]]' = None, **kwargs: 'Any') -> 'Schema_SECOption': """Make IPv4 Security (``SEC``) option. @@ -1671,9 +1652,6 @@ def _make_opt_sec(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_SECOpt kind: option type code option: option data level: classification level - level_default: default value for classification level - level_namespace: namespace for classification level - level_reversed: whether classification level is reversed authorities: list of protection authority flags **kwargs: arbitrary keyword arguments @@ -1681,11 +1659,12 @@ def _make_opt_sec(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_SECOpt Constructed option schema. Raises: - ProtocolError: If ``level`` does not fit in one octet, or - ``authorities`` names a bit position that is not a protection - authority -- a negative one, one that :rfc:`1108` reserves as a - field termination indicator, or one past what the one-octet - option length can count. + ProtocolError: If ``level`` is a name, a :obj:`bool`, or a member + or code that does not fit in one octet, or ``authorities`` + names a bit position that is not a protection authority -- a + negative one, one that :rfc:`1108` reserves as a field + termination indicator, or one past what the one-octet option + length can count. Notes: :rfc:`1108#section-2.4` lays each protection authority octet out as @@ -1706,13 +1685,13 @@ def _make_opt_sec(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_SECOpt """ termination = () # type: tuple[bool, ...] if option is not None: - level_val = option.level + level = option.level authorities = cast('list[Enum_ProtectionAuthority]', option.flags) termination = tuple(getattr(option, 'termination', ())) else: - level_val = self._make_index(level, level_default, namespace=level_namespace, # type: ignore[assignment] - reversed=level_reversed, pack=False) authorities = [] if authorities is None else authorities + level_val = self._make_index(level, Enum_ClassificationLevel, bits=8, + name='classification level') if authorities: for auth in authorities: @@ -1768,7 +1747,7 @@ def _make_opt_sec(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_SECOpt return Schema_SECOption( type=kind, length=self._make_uint(3 + len(data), bits=8, name='option length'), - level=self._make_uint(level_val, bits=8, name='classification level'), + level=level_val, data=data, ) @@ -2213,10 +2192,7 @@ def _make_opt_tr(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_TROptio ) def _make_opt_rtralt(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_RTRALTOption]' = None, *, - alert: 'Enum_RouterAlert | StdlibEnum | AenumEnum | int | str' = Enum_RouterAlert.Aggregated_Reservation_Nesting_Level_0, - alert_default: 'Optional[int]' = None, - alert_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - alert_reversed: 'bool' = False, + alert: 'Enum_RouterAlert | int' = Enum_RouterAlert.Aggregated_Reservation_Nesting_Level_0, **kwargs: 'Any') -> 'Schema_RTRALTOption': """Make IPv4 Router Alert (``RTRALT``) option. @@ -2224,26 +2200,19 @@ def _make_opt_rtralt(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_RTR kind: option type code option: option data alert: router alert type - alert_default: default value for router alert type - alert_namespace: namespace for router alert type - alert_reversed: whether router alert type is reversed **kwargs: arbitrary keyword arguments Returns: Constructed option schema. Raises: - ProtocolError: If the router alert value does not fit in two + ProtocolError: If the router alert value is a name, a + :obj:`bool`, or a member or code that does not fit in two octets. """ - if option is not None: - alert_val = option.alert - else: - alert_val = self._make_index(alert, alert_default, namespace=alert_namespace, # type: ignore[assignment] - reversed=alert_reversed, pack=False) - - self._make_uint(alert_val, bits=16, name='router alert value') + alert_val = self._make_index(alert if option is None else option.alert, Enum_RouterAlert, + bits=16, name='router alert value') return Schema_RTRALTOption( type=kind, length=4, @@ -2251,10 +2220,7 @@ def _make_opt_rtralt(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_RTR ) def _make_opt_qs(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_QuickStartRequestOption | Data_QuickStartReportOption | Data_UnassignedOption]' = None, *, - func: 'Enum_QSFunction | StdlibEnum | AenumEnum | str | int' = Enum_QSFunction.Quick_Start_Request, - func_default: 'Optional[int]' = None, - func_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - func_reversed: 'bool' = False, + func: 'Enum_QSFunction | int' = Enum_QSFunction.Quick_Start_Request, rate: 'int' = 0, ttl: 'timedelta | int' = 0, unused: 'int' = 0, @@ -2267,9 +2233,6 @@ def _make_opt_qs(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_QuickSt kind: option type code option: option data func: QS function type - func_default: default value for QS function type - func_namespace: namespace for QS function type - func_reversed: reversed flag for QS function type rate: rate (in kbps), rounded down to the nearest rate the four-bit rate code carries (:rfc:`4782#section-3.1`) ttl: time to live (in seconds) @@ -2285,23 +2248,22 @@ def _make_opt_qs(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_QuickSt the option takes the eight-octet layout of a Quick-Start Request. Raises: - ProtocolError: If ``func`` does not fit in four bits, the rate code - cannot carry ``rate``, or ``ttl``, ``unused``, ``nonce`` or + ProtocolError: If ``func`` is a name, a :obj:`bool`, or a member + or code that does not fit in four bits, the rate code cannot + carry ``rate``, or ``ttl``, ``unused``, ``nonce`` or ``reserved`` does not fit its field. """ if isinstance(option, Data_UnassignedOption): return cast('Schema_UnassignedOption', self._make_opt_unassigned(kind, option)) if option is not None: - func_enum = option.func + func = option.func rate = option.rate ttl = getattr(option, 'ttl', 0) unused = getattr(option, 'unused', 0) nonce = option.nonce reserved = getattr(option, 'reserved', 0) - else: - func_enum = self._make_index(func, func_default, namespace=func_namespace, # type: ignore[assignment] - reversed=func_reversed, pack=False) + func_enum = self._make_index(func, Enum_QSFunction, bits=4, name='QS function') rate_val = self._make_qs_rate(rate) ttl_value = ttl if isinstance(ttl, int) else math.floor(ttl.total_seconds()) nonce = self._make_uint(nonce, bits=30, name='Quick-Start nonce') @@ -2335,7 +2297,7 @@ def _make_opt_qs(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_QuickSt 'reserved': reserved, }, ) - func_val = self._make_uint(func_enum, bits=4, name='QS function') + func_val = int(func_enum) ttl_value = self._make_uint(ttl_value, bits=8, name='Quick-Start TTL') data = bytes([(func_val << 4) | rate_val, ttl_value]) + ((nonce << 2) | reserved).to_bytes(4, 'big') return cast('Schema_UnassignedOption', self._make_opt_unassigned(kind, data=data)) diff --git a/pcapkit/protocols/internet/ipv6.py b/pcapkit/protocols/internet/ipv6.py index 63d5c47554..bc0a088e0f 100644 --- a/pcapkit/protocols/internet/ipv6.py +++ b/pcapkit/protocols/internet/ipv6.py @@ -45,11 +45,9 @@ from pcapkit.utilities.exceptions import ProtocolError if TYPE_CHECKING: - from enum import IntEnum as StdlibEnum from ipaddress import IPv6Address from typing import IO, Any, ContextManager, Mapping, Optional, Type - from aenum import IntEnum as AenumEnum from typing_extensions import Literal from pcapkit.protocols.protocol import ProtocolBase @@ -93,16 +91,27 @@ class IPv6(IP[Data_IPv6, Schema_IPv6], .. versionchanged:: 1.6.0 - An extension header whose dedicated parser raises is the upper layer, - parsed once as :class:`~pcapkit.protocols.misc.raw.Raw`, and the chain - walk stops there, as :rfc:`8200#section-4` has a node discard such a - packet (:issue:`2477`). + * An extension header whose dedicated parser raises is the upper + layer, parsed once as :class:`~pcapkit.protocols.misc.raw.Raw`, and + the chain walk stops there, as :rfc:`8200#section-4` has a node + discard such a packet (:issue:`2477`). + * ``next`` of :meth:`make` takes a member or an :class:`int` code, + resolved with the enumeration's ``get``; a name, a :obj:`bool`, or a + code that does not fit in one octet raises + :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than + resolving against an enumeration inferred from the caller's frame or + writing a different value (:issue:`2594`). .. versionremoved:: 1.6.0 - ``__generic_ext_codes__``, the extension headers whose dedicated parser - raising fell back to - :class:`~pcapkit.protocols.internet.ipv6_ext.IPv6_Ext` (:issue:`2477`). + * ``__generic_ext_codes__``, the extension headers whose dedicated + parser raising fell back to + :class:`~pcapkit.protocols.internet.ipv6_ext.IPv6_Ext` + (:issue:`2477`). + * The ``next_default``, ``next_namespace`` and ``next_reversed`` + arguments of :meth:`make`; resolve a name with + :meth:`~pcapkit.protocols.protocol.Protocol.resolve_index` first + (:issue:`2594`). """ @@ -254,10 +263,7 @@ def read(self, length: 'Optional[int]' = None, *, def make(self, traffic_class: 'int' = 0, flow_label: 'int' = 0, - next: 'Enum_TransType | StdlibEnum | AenumEnum | str | int' = Enum_TransType.UDP, - next_default: 'Optional[int]' = None, - next_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - next_reversed: 'bool' = False, + next: 'Enum_TransType | int' = Enum_TransType.UDP, hop_limit: 'int' = 64, # reasonable default src: 'IPv6Address | str | bytes | int' = '::1', dst: 'IPv6Address | str | bytes | int' = '::', @@ -271,9 +277,6 @@ def make(self, traffic_class: Traffic class. flow_label: Flow label. next: Next header. - next_default: Default value of next header. - next_namespace: Namespace of next header. - next_reversed: Whether to reverse the namespace of next header. hop_limit: Hop limit. src: Source IP address. dst: Destination IP address. @@ -289,14 +292,17 @@ def make(self, Returns: Constructed packet data. + Raises: + ProtocolError: If ``next`` is a name, a :obj:`bool`, or a member + or code that does not fit in one octet. + .. versionchanged:: 1.5.0 Takes ``payload_length`` ahead of ``payload`` (:issue:`1155`), which shifts a positional ``payload``. """ - next_val = self._make_index(next, next_default, namespace=next_namespace, - reversed=next_reversed, pack=False) + next_val = self._make_index(next, Enum_TransType, bits=8, name='next header') # NOTE: An explicit ``payload_length`` is written as given, so that a # ``from_data`` rebuild of a capture truncated by snaplen keeps the @@ -319,7 +325,7 @@ def make(self, 'label': flow_label, }, length=payload_length, - next=next_val, # type: ignore[arg-type] + next=next_val, limit=hop_limit, src=src, dst=dst, diff --git a/pcapkit/protocols/internet/ipv6_ext.py b/pcapkit/protocols/internet/ipv6_ext.py index 08affb757d..e5f94971ea 100644 --- a/pcapkit/protocols/internet/ipv6_ext.py +++ b/pcapkit/protocols/internet/ipv6_ext.py @@ -231,11 +231,16 @@ class keywords implement it. .. versionchanged:: 1.6.0 - The library no longer passes ``error`` to :meth:`read`, so - :attr:`Data_IPv6_Ext.error ` - is :data:`None` unless a caller passes one. A header whose dedicated - parser raises is :class:`~pcapkit.protocols.misc.raw.Raw`, whose - ``info.error`` holds the error (:issue:`2477`). + * The library does not pass ``error`` to :meth:`read`, so + :attr:`Data_IPv6_Ext.error ` + is :data:`None` unless a caller passes one. A header whose dedicated + parser raises is :class:`~pcapkit.protocols.misc.raw.Raw`, whose + ``info.error`` holds the error (:issue:`2477`). + * ``next`` of :meth:`make` takes a member or an :class:`int` code, + resolved with the enumeration's ``get``; a name, a :obj:`bool`, or a + code that does not fit in one octet raises + :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than + being handed to the schema unchecked (:issue:`2594`). """ @@ -514,6 +519,10 @@ def make(self, *, Returns: Constructed packet data. + Raises: + ProtocolError: If ``next`` is a name, a :obj:`bool`, or a member + or code that does not fit in one octet. + Note: **Declared, and keyword-only**, each for its own reason. @@ -561,7 +570,7 @@ def make(self, *, payload = data + payload return cast('_ST', Schema_IPv6_Ext( - next=next, + next=self._make_index(next, Enum_TransType, bits=8, name='next header'), len=len, payload=payload, )) diff --git a/pcapkit/protocols/internet/ipv6_frag.py b/pcapkit/protocols/internet/ipv6_frag.py index 9fbbe91147..51729b3302 100644 --- a/pcapkit/protocols/internet/ipv6_frag.py +++ b/pcapkit/protocols/internet/ipv6_frag.py @@ -34,10 +34,8 @@ from pcapkit.utilities.exceptions import UnsupportedCall if TYPE_CHECKING: - from enum import IntEnum as StdlibEnum - from typing import IO, Any, NoReturn, Optional, Type + from typing import IO, Any, NoReturn, Optional - from aenum import IntEnum as AenumEnum from typing_extensions import Literal from pcapkit.corekit.protochain import ProtoChain @@ -55,6 +53,22 @@ class IPv6_Frag(IPv6_Ext[Data_IPv6_Frag, Schema_IPv6_Frag], A rebuild keeps the reserved bits (:issue:`1235`). + .. versionchanged:: 1.6.0 + + ``next`` of :meth:`make` takes a member or an :class:`int` code, + resolved with the enumeration's ``get``; a name, a :obj:`bool`, or a + code that does not fit in one octet raises + :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than + resolving against an enumeration inferred from the caller's frame or + writing a different value (:issue:`2594`). + + .. versionremoved:: 1.6.0 + + The ``next_default``, ``next_namespace`` and ``next_reversed`` + arguments of :meth:`make`; resolve a name with + :meth:`~pcapkit.protocols.protocol.Protocol.resolve_index` first + (:issue:`2594`). + """ ########################################################################## @@ -166,10 +180,7 @@ def read(self, length: 'Optional[int]' = None, *, extension: 'bool' = False, # return self._decode_next_layer(ipv6_frag, schema.next, length - self.length) def make(self, - next: 'Enum_TransType | StdlibEnum | AenumEnum | str | int' = Enum_TransType.UDP, - next_default: 'Optional[int]' = None, - next_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - next_reversed: 'bool' = False, + next: 'Enum_TransType | int' = Enum_TransType.UDP, reserved_octet: 'bytes' = b'\x00', offset: 'int' = 0, reserved: 'int' = 0, @@ -181,9 +192,6 @@ def make(self, Args: next: Next header. - next_default: Default value of next header. - next_namespace: Namespace of next header. - next_reversed: If the namespace of next header is reversed. reserved_octet: The reserved octet after the next header, truncated or zero-filled to one octet. offset: Fragment offset, in on-wire 8-octet units (:rfc:`8200#section-4.5`). @@ -199,6 +207,10 @@ def make(self, Returns: Constructed packet data. + Raises: + ProtocolError: If ``next`` is a name, a :obj:`bool`, or a member + or code that does not fit in one octet. + .. versionchanged:: 1.5.0 Takes ``reserved_octet`` ahead of ``offset`` (:issue:`1223`) and @@ -206,11 +218,10 @@ def make(self, later positional argument. """ - next_val = self._make_index(next, next_default, namespace=next_namespace, - reversed=next_reversed, pack=False) + next_val = self._make_index(next, Enum_TransType, bits=8, name='next header') return Schema_IPv6_Frag( - next=next_val, # type: ignore[arg-type] + next=next_val, reserved=reserved_octet, flags={ 'offset': offset, diff --git a/pcapkit/protocols/internet/ipv6_opts.py b/pcapkit/protocols/internet/ipv6_opts.py index 5759778da3..803053efb5 100644 --- a/pcapkit/protocols/internet/ipv6_opts.py +++ b/pcapkit/protocols/internet/ipv6_opts.py @@ -100,11 +100,9 @@ if TYPE_CHECKING: from datetime import timedelta - from enum import IntEnum as StdlibEnum from ipaddress import IPv4Address, IPv6Address - from typing import IO, Any, Callable, DefaultDict, NoReturn, Optional, Type + from typing import IO, Any, Callable, DefaultDict, NoReturn, Optional - from aenum import IntEnum as AenumEnum from mypy_extensions import DefaultArg, KwArg, NamedArg from typing_extensions import Literal @@ -227,6 +225,24 @@ class IPv6_Opts(IPv6_Ext[Data_IPv6_Opts, Schema_IPv6_Opts], * A PDM scale, given or derived from its delta, that does not fit its octet raises :exc:`~pcapkit.utilities.exceptions.ProtocolError`, where it warned and was written wrapped (:issue:`2614`). + * Each enumeration field of :meth:`make` and of the option makers, + including the type of each option given to :meth:`make`, takes a + member or an :class:`int` code, resolved with the enumeration's + ``get``; a name, a :obj:`bool` for a field wider than one bit, or a + code the field cannot carry raises + :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than + resolving against an enumeration inferred from the caller's frame, + writing a different value or raising another exception + (:issue:`2594`). + + .. versionremoved:: 1.6.0 + + The ``*_default``, ``*_namespace`` and ``*_reversed`` arguments that + went with each enumeration field of :meth:`make` and of the option + makers -- ``next``, ``alert``, ``mode``, ``tid_type``, ``func`` and + ``seed_type``; resolve a name with + :meth:`~pcapkit.protocols.protocol.Protocol.resolve_index` first + (:issue:`2594`). """ @@ -365,20 +381,14 @@ def read(self, length: 'Optional[int]' = None, *, extension: 'bool' = False, # return self._decode_next_layer(ipv6_opts, schema.next, length - ipv6_opts.length) def make(self, - next: 'Enum_TransType | StdlibEnum | AenumEnum | str | int' = Enum_TransType.UDP, - next_default: 'Optional[int]' = None, - next_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - next_reversed: 'bool' = False, - options: 'Optional[list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes] | Option]' = None, # pylint: disable=line-too-long + next: 'Enum_TransType | int' = Enum_TransType.UDP, + options: 'Optional[list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes] | Option]' = None, # pylint: disable=line-too-long payload: 'bytes | ProtocolBase | Schema' = b'', **kwargs: 'Any') -> 'Schema_IPv6_Opts': """Make (construct) packet data. Args: next: Next header type. - next_default: Default value of next header type. - next_namespace: Namespace of next header type. - next_reversed: If the namespace of next header type is reversed. options: Destination Options. payload: Payload of current protocol. **kwargs: Arbitrary keyword arguments. @@ -387,12 +397,13 @@ def make(self, Constructed packet data. Raises: - ProtocolError: If the options make a header too long for its + ProtocolError: If ``next``, or the type of an option, is a name, a + :obj:`bool`, or a member or code that does not fit in one + octet; or if the options make a header too long for its one-octet ``Hdr Ext Len``. """ - next_value = self._make_index(next, next_default, namespace=next_namespace, - reversed=next_reversed, pack=False) + next_value = self._make_index(next, Enum_TransType, bits=8, name='next header') # NOTE: No options is not the same as no options area. The header is at # least 8 octets, 2 of them the fixed part, so the remaining 6 must be @@ -407,7 +418,7 @@ def make(self, length = self._make_uint((total_length - 6) // 8, bits=8, name='Hdr Ext Len') return Schema_IPv6_Opts( - next=next_value, # type: ignore[arg-type] + next=next_value, len=length, options=options_value, payload=payload, @@ -609,7 +620,7 @@ def _read_ipv6_opts(self, length: 'int') -> 'Option': raise ProtocolError('IPv6-Opts: invalid format') return options - def _make_ipv6_opts(self, options: 'list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes] | Option') -> 'tuple[list[Schema_Option | bytes], int]': + def _make_ipv6_opts(self, options: 'list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes] | Option') -> 'tuple[list[Schema_Option | bytes], int]': # pylint: disable=line-too-long """Make options for IPv6-Opts. Args: @@ -653,7 +664,8 @@ def _make_ipv6_opts(self, options: 'list[Schema_Option | tuple[Enum_Option, dict opt = schema opt_len = len(schema.pack()) else: - code, args = cast('tuple[Enum_Option, dict[str, Any]]', schema) + kind, args = cast('tuple[Enum_Option | int, dict[str, Any]]', schema) + code = self._make_index(kind, Enum_Option, bits=8, name='option type') if code in (Enum_Option.Pad1, Enum_Option.PadN): # ignore padding options by default continue @@ -686,7 +698,8 @@ def _make_ipv6_opts(self, options: 'list[Schema_Option | tuple[Enum_Option, dict # included and in place, so that a parsed header rebuilds byte for # byte; only a header left short of alignment gets trailing padding. options_list = [] - for code, option in options.items(multi=True): + for key, option in options.items(multi=True): + code = self._make_index(key, Enum_Option, bits=8, name='option type') name = self._lookup_registry(self.__option__, code) if isinstance(name, str): meth_name = f'_make_opt_{name}' @@ -1606,10 +1619,7 @@ def _make_opt_tun(self, code: 'Enum_Option', opt: 'Optional[Data_TunnelEncapsula ) def _make_opt_ra(self, code: 'Enum_Option', opt: 'Optional[Data_RouterAlertOption]' = None, *, - alert: 'Enum_RouterAlert | StdlibEnum | AenumEnum | str | int' = Enum_RouterAlert.Datagram_contains_a_Multicast_Listener_Discovery_message, # pylint: disable=line-too-long - alert_default: 'Optional[int]' = None, - alert_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - alert_reversed: 'bool' = False, + alert: 'Enum_RouterAlert | int' = Enum_RouterAlert.Datagram_contains_a_Multicast_Listener_Discovery_message, # pylint: disable=line-too-long **kwargs: 'Any') -> 'Schema_RouterAlertOption': """Make IPv6-Opts router alert option. @@ -1617,26 +1627,18 @@ def _make_opt_ra(self, code: 'Enum_Option', opt: 'Optional[Data_RouterAlertOptio code: option type value opt: option data alert: router alert value - alert_default: default value of router alert - alert_namespace: namespace of router alert - alert_reversed: reversed flag of router alert **kwargs: arbitrary keyword arguments Returns: Constructed option schema. Raises: - ProtocolError: If the router alert value does not fit in two - octets. + ProtocolError: If the router alert value is a name, a :obj:`bool`, + or a member or code that does not fit in two octets. """ - if opt is not None: - value = opt.value - else: - value = self._make_index(alert, alert_default, namespace=alert_namespace, # type: ignore[assignment] - reversed=alert_reversed, pack=False) - - self._make_uint(value, bits=16, name='router alert value') + value = self._make_index(opt.value if opt is not None else alert, Enum_RouterAlert, + bits=16, name='router alert value') return Schema_RouterAlertOption( type=code, len=2, @@ -1725,14 +1727,8 @@ def _make_opt_calipso(self, code: 'Enum_Option', opt: 'Optional[Data_CALIPSOOpti ) def _make_opt_smf_dpd(self, code: 'Enum_Option', opt: 'Optional[Data_SMFIdentificationBasedDPDOption | Data_SMFHashBasedDPDOption]' = None, *, - mode: 'Enum_SMFDPDMode | StdlibEnum | AenumEnum | str | int' = Enum_SMFDPDMode.I_DPD, - mode_default: 'Optional[int]' = None, - mode_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - mode_reversed: 'bool' = False, - tid_type: 'Optional[Enum_TaggerID | StdlibEnum | AenumEnum | str | int]' = None, - tid_type_default: 'Optional[int]' = None, - tid_type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - tid_type_reversed: 'bool' = False, + mode: 'Enum_SMFDPDMode | int' = Enum_SMFDPDMode.I_DPD, + tid_type: 'Optional[Enum_TaggerID | int]' = None, tid: 'Optional[bytes | IPv4Address | IPv6Address]' = None, id: 'bytes' = b'', hav: 'bytes' = b'', @@ -1743,14 +1739,8 @@ def _make_opt_smf_dpd(self, code: 'Enum_Option', opt: 'Optional[Data_SMFIdentifi code: option type value opt: option data mode: DPD mode - mode_default: default value of DPD mode - mode_namespace: namespace of DPD mode - mode_reversed: reversed flag of DPD mode tid_type: TaggerID type (``TidTy`` field); inferred from the length of ``tid`` if not given - tid_type_default: default value of TaggerID type - tid_type_namespace: namespace of TaggerID type - tid_type_reversed: reversed flag of TaggerID type tid: Tagger ID id: identifier hav: hash assist value (HAV) @@ -1760,10 +1750,12 @@ def _make_opt_smf_dpd(self, code: 'Enum_Option', opt: 'Optional[Data_SMFIdentifi Constructed option schema. Raises: - ProtocolError: If the DPD mode is invalid, the Tagger ID does - not fit the TaggerID type, the H-DPD Hash Assist Value is - empty, or the option data is too long for its one-octet - ``Opt Data Len``. + ProtocolError: If the DPD mode is a name, or a member or code + that does not fit in one bit; if the TaggerID type is a name, + a :obj:`bool`, or a member or code that does not fit in three + bits; if the Tagger ID does not fit the TaggerID type, the + H-DPD Hash Assist Value is empty, or the option data is too + long for its one-octet ``Opt Data Len``. """ if opt is not None: @@ -1773,8 +1765,7 @@ def _make_opt_smf_dpd(self, code: 'Enum_Option', opt: 'Optional[Data_SMFIdentifi id = getattr(opt, 'id', b'') hav = getattr(opt, 'hav', b'') - dpd_type = self._make_index(mode, mode_default, namespace=mode_namespace, - reversed=mode_reversed, pack=False) + dpd_type = self._make_index(mode, Enum_SMFDPDMode, bits=1, name='DPD mode') if dpd_type == Enum_SMFDPDMode.I_DPD: if tid is None: @@ -1788,13 +1779,8 @@ def _make_opt_smf_dpd(self, code: 'Enum_Option', opt: 'Optional[Data_SMFIdentifi tid_len = len(tid_bytes) if tid_type is not None: - # NOTE: ``tid_type`` defaults to :data:`None`, so ``_make_index`` has no - # enum default to infer the namespace of a :obj:`str` member name from; - # name it here rather than leave the name unresolvable (#2356). - tid_kind = self._make_index( - tid_type, tid_type_default, - namespace=tid_type_namespace if tid_type_namespace is not None else Enum_TaggerID, - reversed=tid_type_reversed, pack=False) + tid_kind = self._make_index(tid_type, Enum_TaggerID, bits=3, + name='TaggerID type') # type: Enum_TaggerID | int elif tid_len == 0: tid_kind = Enum_TaggerID.NULL elif tid_len == 4: @@ -1828,8 +1814,9 @@ def _make_opt_smf_dpd(self, code: 'Enum_Option', opt: 'Optional[Data_SMFIdentifi tid=tid_bytes if tid_len else None, id=id, ) # type: Schema_SMFDPDOption - elif dpd_type == Enum_SMFDPDMode.H_DPD: - # The first octet of the HAV carries the H-DPD mode bit. + else: + # H-DPD, the other value of the one-bit mode. The first octet of + # the HAV carries the H-DPD mode bit. if not hav: raise ProtocolError(f'{self.alias}: [OptNo {code}] empty Hash Assist Value (HAV)') hav_ba = bytearray(hav) @@ -1840,8 +1827,6 @@ def _make_opt_smf_dpd(self, code: 'Enum_Option', opt: 'Optional[Data_SMFIdentifi len=self._make_uint(len(hav), bits=8, name='Opt Data Len'), hav=bytes(hav_ba), ) - else: - raise ProtocolError(f'{self.alias}: [OptNo {code}] invalid DPD type: {dpd_type}') return schema def _make_opt_pdm(self, code: 'Enum_Option', opt: 'Optional[Data_PDMOption]' = None, *, @@ -1912,10 +1897,7 @@ def _make_opt_pdm(self, code: 'Enum_Option', opt: 'Optional[Data_PDMOption]' = N ) def _make_opt_qs(self, code: 'Enum_Option', opt: 'Optional[Data_QuickStartOption | Data_UnassignedOption]' = None, *, # pylint: disable=line-too-long - func: 'Enum_QSFunction | StdlibEnum | AenumEnum | str | int' = Enum_QSFunction.Quick_Start_Request, - func_default: 'Optional[int]' = None, - func_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - func_reversed: 'bool' = False, + func: 'Enum_QSFunction | int' = Enum_QSFunction.Quick_Start_Request, rate: 'int' = 0, ttl: 'timedelta | int' = 0, nonce: 'int' = 0, @@ -1928,9 +1910,6 @@ def _make_opt_qs(self, code: 'Enum_Option', opt: 'Optional[Data_QuickStartOption code: option type value opt: option data func: QS function type - func_default: default value for QS function type - func_namespace: namespace for QS function type - func_reversed: reversed flag for QS function type rate: rate (in kbps), rounded down to the nearest rate the four-bit rate code carries (:rfc:`4782#section-3.1`) ttl: time to live (in seconds) @@ -1946,9 +1925,10 @@ def _make_opt_qs(self, code: 'Enum_Option', opt: 'Optional[Data_QuickStartOption the option data takes the layout of a Quick-Start Request. Raises: - ProtocolError: If ``func`` does not fit in four bits, the rate code - cannot carry ``rate``, or ``ttl``, ``nonce`` or ``reserved`` does - not fit its field. + ProtocolError: If ``func`` is a name, a :obj:`bool`, or a member or + code that does not fit in four bits, the rate code cannot carry + ``rate``, or ``ttl``, ``nonce`` or ``reserved`` does not fit its + field. """ if isinstance(opt, Data_UnassignedOption): @@ -1961,8 +1941,7 @@ def _make_opt_qs(self, code: 'Enum_Option', opt: 'Optional[Data_QuickStartOption reserved = getattr(opt, 'reserved', 0) unused = getattr(opt, 'unused', b'\x00') else: - func_enum = self._make_index(func, func_default, namespace=func_namespace, # type: ignore[assignment] - reversed=func_reversed, pack=False) + func_enum = self._make_index(func, Enum_QSFunction, bits=4, name='QS function') rate_val = self._make_qs_rate(rate) ttl_value = ttl if isinstance(ttl, int) else math.floor(ttl.total_seconds()) nonce = self._make_uint(nonce, bits=30, name='Quick-Start nonce') @@ -2062,10 +2041,7 @@ def _make_opt_rpl(self, code: 'Enum_Option', opt: 'Optional[Data_RPLOption]' = N ) def _make_opt_mpl(self, code: 'Enum_Option', opt: 'Optional[Data_MPLOption]' = None, *, - seed_type: 'Optional[Enum_SeedID | StdlibEnum | AenumEnum | str | int]' = None, - seed_type_default: 'Optional[int]' = None, - seed_type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - seed_type_reversed: 'bool' = False, + seed_type: 'Optional[Enum_SeedID | int]' = None, max: 'bool' = False, drop: 'bool' = False, reserved: 'int' = 0, @@ -2078,9 +2054,6 @@ def _make_opt_mpl(self, code: 'Enum_Option', opt: 'Optional[Data_MPLOption]' = N code: option type value opt: option data seed_type: seed-id type (``S`` field); inferred from ``seed`` if not given - seed_type_default: default value of seed-id type - seed_type_namespace: namespace of seed-id type - seed_type_reversed: reversed flag of seed-id type max: maximum sequence number flag drop: drop packet flag reserved: reserved bits (``rsv``), i.e. the low four bits of the @@ -2095,8 +2068,10 @@ def _make_opt_mpl(self, code: 'Enum_Option', opt: 'Optional[Data_MPLOption]' = N Constructed option schema. Raises: - ProtocolError: If the seed ID is missing or does not fit the seed-id - type, or ``reserved`` or ``seq`` does not fit its field. + ProtocolError: If the seed-id type is a name, a :obj:`bool`, or a + member or code that does not fit in two bits; if the seed ID is + missing or does not fit the seed-id type, or ``reserved`` or + ``seq`` does not fit its field. """ if opt is not None: @@ -2111,13 +2086,8 @@ def _make_opt_mpl(self, code: 'Enum_Option', opt: 'Optional[Data_MPLOption]' = N seq = self._make_uint(seq, bits=8, name='MPL sequence number') if seed_type is not None: - # NOTE: ``seed_type`` defaults to :data:`None`, so ``_make_index`` has no - # enum default to infer the namespace of a :obj:`str` member name from; - # name it here rather than leave the name unresolvable (#2356). - kind = self._make_index( - seed_type, seed_type_default, - namespace=seed_type_namespace if seed_type_namespace is not None else Enum_SeedID, - reversed=seed_type_reversed, pack=False) + kind = self._make_index(seed_type, Enum_SeedID, bits=2, + name='MPL seed-id type') # type: Enum_SeedID | int elif seed is None or isinstance(seed, ipaddress.IPv6Address): kind = Enum_SeedID.IPV6_SOURCE_ADDRESS else: @@ -2138,10 +2108,8 @@ def _make_opt_mpl(self, code: 'Enum_Option', opt: 'Optional[Data_MPLOption]' = N clen = 4 elif kind == Enum_SeedID.SEEDID_64_BIT_UNSIGNED_INTEGER: clen = 10 - elif kind == Enum_SeedID.SEEDID_128_BIT_UNSIGNED_INTEGER: + else: # the 128-bit seed, the last value of the two-bit type clen = 18 - else: - raise ProtocolError(f'{self.alias}: [OptNo {code}] invalid MPL seed-id type: {kind}') if seed is None: raise ProtocolError(f'{self.alias}: [OptNo {code}] missing MPL seed ID for type {kind}') diff --git a/pcapkit/protocols/internet/ipv6_route.py b/pcapkit/protocols/internet/ipv6_route.py index 001833997f..9fe27c161e 100644 --- a/pcapkit/protocols/internet/ipv6_route.py +++ b/pcapkit/protocols/internet/ipv6_route.py @@ -51,11 +51,9 @@ from pcapkit.utilities.warnings import RegistryWarning, warn if TYPE_CHECKING: - from enum import IntEnum as StdlibEnum from ipaddress import IPv6Address - from typing import IO, Any, Callable, DefaultDict, NoReturn, Optional, Type + from typing import IO, Any, Callable, DefaultDict, NoReturn, Optional - from aenum import IntEnum as AenumEnum from mypy_extensions import DefaultArg, KwArg, NamedArg from typing_extensions import Literal @@ -119,13 +117,26 @@ class IPv6_Route(IPv6_Ext[Data_IPv6_Route, Schema_IPv6_Route], .. versionchanged:: 1.6.0 - :meth:`make` and the RPL source route maker, given a value a - fixed-width field cannot carry -- negative, too wide, or a :obj:`bool` - for a field wider than one bit -- raise - :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than writing - a different value or raising another exception. The registry codes - :meth:`make` takes, ``next`` and ``type``, are not range-checked - (:issue:`2614`). + * :meth:`make` and the RPL source route maker, given a value a + fixed-width field cannot carry -- negative, too wide, or a + :obj:`bool` for a field wider than one bit -- raise + :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than + writing a different value or raising another exception + (:issue:`2614`). + * Each enumeration field of :meth:`make`, ``next`` and ``type``, takes + a member or an :class:`int` code, resolved with the enumeration's + ``get``; a name, a :obj:`bool`, or a code that does not fit in one + octet raises :exc:`~pcapkit.utilities.exceptions.ProtocolError`, + rather than resolving against an enumeration inferred from the + caller's frame or writing a different value (:issue:`2594`). + + .. versionremoved:: 1.6.0 + + The ``next_default``, ``next_namespace``, ``next_reversed``, + ``type_default``, ``type_namespace`` and ``type_reversed`` arguments of + :meth:`make`; resolve a name with + :meth:`~pcapkit.protocols.protocol.Protocol.resolve_index` first + (:issue:`2594`). """ @@ -254,14 +265,8 @@ def read(self, length: 'Optional[int]' = None, *, extension: 'bool' = False, # def make(self, dst: 'Optional[IPv6Address | str | int| bytes]' = None, - next: 'Enum_TransType | StdlibEnum | AenumEnum | str | int' = Enum_TransType.UDP, - next_default: 'Optional[int]' = None, - next_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - next_reversed: 'bool' = False, - type: 'Enum_Routing | StdlibEnum | AenumEnum | str | int' = Enum_Routing.Source_Route, - type_default: 'Optional[int]' = None, - type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - type_reversed: 'bool' = False, + next: 'Enum_TransType | int' = Enum_TransType.UDP, + type: 'Enum_Routing | int' = Enum_Routing.Source_Route, seg_left: 'int' = 0, data: 'bytes | Data_IPv6_Route | Schema_RoutingType | dict[str, Any]' = b'\x00\x00\x00\x00', hdr_ext_len: 'Optional[int]' = None, @@ -272,13 +277,7 @@ def make(self, Args: dst: Destination address. next: Next header type. - next_default: Default value of next header type. - next_namespace: Namespace of next header type. - next_reversed: If the namespace of next header type is reversed. type: Routing type. - type_default: Default value of routing type. - type_namespace: Namespace of routing type. - type_reversed: If the namespace of routing type is reversed. seg_left: Segments left. data: Routing data. hdr_ext_len: ``Hdr Ext Len`` field value; computed from ``data`` @@ -294,11 +293,12 @@ def make(self, :func:`~pcapkit.corekit.fields.ipaddress.parse_ip_address`) and ``data`` is a :obj:`dict` or a data model; ``dst`` is not read for any other ``data``. - ProtocolError: If ``data`` is :obj:`bytes` that do not parse as - the routing data of ``type``, or is of none of the accepted - types; or if ``seg_left``, or ``hdr_ext_len`` or the - ``Hdr Ext Len`` computed from ``data``, does not fit in one - octet. + ProtocolError: If ``next`` or ``type`` is a name, a :obj:`bool`, + or a member or code that does not fit in one octet; if + ``data`` is :obj:`bytes` that do not parse as the routing data + of ``type``, or is of none of the accepted types; or if + ``seg_left``, or ``hdr_ext_len`` or the ``Hdr Ext Len`` + computed from ``data``, does not fit in one octet. .. versionchanged:: 1.5.0 @@ -306,12 +306,8 @@ def make(self, shifts a positional ``payload``. """ - next_val = cast('Enum_TransType', - self._make_index(next, next_default, namespace=next_namespace, - reversed=next_reversed, pack=False)) - type_val = cast('Enum_Routing', - self._make_index(type, type_default, namespace=type_namespace, - reversed=type_reversed, pack=False)) + next_val = self._make_index(next, Enum_TransType, bits=8, name='next header') + type_val = self._make_index(type, Enum_Routing, bits=8, name='routing type') if isinstance(data, bytes): # ``data`` here *is* the type-specific data (no per-type diff --git a/pcapkit/protocols/internet/ipx.py b/pcapkit/protocols/internet/ipx.py index b0ddb7d377..a01087a0f0 100644 --- a/pcapkit/protocols/internet/ipx.py +++ b/pcapkit/protocols/internet/ipx.py @@ -39,10 +39,8 @@ from pcapkit.utilities.warnings import RegistryWarning, warn if TYPE_CHECKING: - from enum import IntEnum as StdlibEnum from typing import Any, ClassVar, DefaultDict, Optional, Type - from aenum import IntEnum as AenumEnum from typing_extensions import Literal from pcapkit.corekit.enum import EnumRegistry @@ -69,6 +67,22 @@ class IPX(Internet[Data_IPX, Schema_IPX], :attr:`__proto__`, rather than looked up as an IP protocol number (:issue:`2169`). + .. versionchanged:: 1.6.0 + + The ``type`` of :meth:`make` takes a member or an :class:`int` code, + resolved with the enumeration's ``get``; a name, a :obj:`bool`, or a + code the field cannot carry raises + :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than + resolving against an enumeration inferred from the caller's frame or + writing a different value (:issue:`2594`). + + .. versionremoved:: 1.6.0 + + The ``type_default``, ``type_namespace`` and ``type_reversed`` + arguments that went with the ``type`` of :meth:`make`; resolve a name + with :meth:`~pcapkit.protocols.protocol.Protocol.resolve_index` first + (:issue:`2594`). + """ ########################################################################## @@ -170,10 +184,7 @@ def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_IPX': def make(self, chksum: 'bytes' = b'\x00\x00', count: 'int' = 0, - type: 'Enum_Packet | StdlibEnum | AenumEnum | str | int' = Enum_Packet.Unknown, - type_default: 'Optional[int]' = None, - type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - type_reversed: 'bool' = False, + type: 'Enum_Packet | int' = Enum_Packet.Unknown, total_length: 'Optional[int]' = None, dst: 'bytes' = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', src: 'bytes' = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', @@ -186,9 +197,6 @@ def make(self, chksum: Checksum. count: Transport Control (hop count). type: Packet Type. - type_default: Default value for undefined packet type. - type_namespace: Namespace for packet type. - type_reversed: Reverse namespace for packet type. total_length: Packet length (header included); computed as ``30`` plus the payload length when not given. dst: Destination Address. @@ -201,14 +209,17 @@ def make(self, Returns: Constructed packet data. + Raises: + ProtocolError: If ``type`` is a name, a :obj:`bool`, or a member + or code that does not fit in one octet. + .. versionchanged:: 1.5.0 Takes ``total_length`` ahead of ``dst`` (:issue:`1315`), which shifts every later positional argument. """ - type_val = self._make_index(type, type_default, namespace=type_namespace, - reversed=type_reversed, pack=False) + type_val = self._make_index(type, Enum_Packet, bits=8, name='packet type') if total_length is None: total_length = 30 + len(payload) @@ -216,7 +227,7 @@ def make(self, chksum=chksum, len=total_length, count=count, - type=type_val, # type: ignore[arg-type] + type=type_val, dst=dst, src=src, payload=payload, diff --git a/pcapkit/protocols/internet/mh.py b/pcapkit/protocols/internet/mh.py index fe7a5f7f0a..b09c73f4c4 100644 --- a/pcapkit/protocols/internet/mh.py +++ b/pcapkit/protocols/internet/mh.py @@ -505,11 +505,9 @@ if TYPE_CHECKING: from datetime import datetime as dt_type from datetime import timedelta - from enum import IntEnum as StdlibEnum from ipaddress import IPv4Address, IPv6Address, IPv6Network - from typing import IO, Any, Callable, DefaultDict, NoReturn, Optional, Type + from typing import IO, Any, Callable, DefaultDict, NoReturn, Optional - from aenum import IntEnum as AenumEnum from mypy_extensions import DefaultArg, KwArg, NamedArg from typing_extensions import Literal @@ -1141,22 +1139,38 @@ class MH(IPv6_Ext[Data_MH, Schema_MH], .. versionchanged:: 1.6.0 - :meth:`make` and the message, option and sub-option makers, given an - integer argument its field cannot carry -- negative, too wide, or a - :obj:`bool` for a field wider than one bit -- raise - :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than - writing a different value or raising another exception; so does the - access network identifier sub-option maker for a raw geo-location - degree that does not fit 24 bits as two's complement, where it masked - it. The option makers check their registry codes and their one-octet - ``Option Length`` too; the message makers' registry codes are not - range-checked (:issue:`2614`). + * :meth:`make` and the message, option and sub-option makers, given an + integer argument its field cannot carry -- negative, too wide, or a + :obj:`bool` for a field wider than one bit -- raise + :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than + writing a different value or raising another exception; so does the + access network identifier sub-option maker for a raw geo-location + degree that does not fit 24 bits as two's complement, where it masked + it. The option makers check their registry codes and their one-octet + ``Option Length`` too (:issue:`2614`). + * Each enumeration field of :meth:`make` and of the message, option and + sub-option makers, and the type of each option, sub-option, QoS + attribute and CGA extension given to them, takes a member or an + :class:`int` code, resolved with the enumeration's ``get``; a name, a + :obj:`bool` for a field wider than one bit, or a code the field cannot + carry raises :exc:`~pcapkit.utilities.exceptions.ProtocolError`, + rather than resolving against an enumeration inferred from the + caller's frame or writing a different value. The Binding Error + maker resolves ``status`` with + :class:`~pcapkit.const.mh.binding_error.BindingError`, the registry + its schema holds, rather than with + :class:`~pcapkit.const.mh.status_code.StatusCode` (:issue:`2594`). .. versionremoved:: 1.6.0 - ``_decode_signed``, the two's-complement decoder of the geo-location - degrees; :meth:`~pcapkit.protocols.protocol.Protocol._read_sint` reads - them instead (:issue:`2614`). + * ``_decode_signed``, the two's-complement decoder of the geo-location + degrees; :meth:`~pcapkit.protocols.protocol.Protocol._read_sint` reads + them instead (:issue:`2614`). + * The ``X_default``, ``X_namespace`` and ``X_reversed`` arguments that + went with each enumeration field ``X`` of :meth:`make` and of the + message and option makers; resolve a name with + :meth:`~pcapkit.protocols.protocol.Protocol.resolve_index` first + (:issue:`2594`). """ @@ -1434,14 +1448,8 @@ def read(self, length: 'Optional[int]' = None, *, version: 'Literal[4, 6]' = 4, return self._decode_next_layer(mh, schema.next, length - mh.length) def make(self, - next: 'Enum_TransType | StdlibEnum | AenumEnum | str | int' = Enum_TransType.UDP, - next_default: 'Optional[int]' = None, - next_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - next_reversed: 'bool' = False, - type: 'Enum_Packet | StdlibEnum | AenumEnum | str | int' = Enum_Packet.Binding_Refresh_Request, - type_default: 'Optional[int]' = None, - type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - type_reversed: 'bool' = False, + next: 'Enum_TransType | int' = Enum_TransType.UDP, + type: 'Enum_Packet | int' = Enum_Packet.Binding_Refresh_Request, reserved: 'int' = 0, chksum: 'bytes' = b'', data: 'bytes | Data_MH | Schema_Packet | dict[str, Any]' = b'\x00\x00', # minimum length @@ -1451,13 +1459,7 @@ def make(self, Args: next: Next header type. - next_default: Default value for next header type field. - next_namespace: Namespace of next header type field. - next_reversed: Whether the namespace of next header type field is reversed. type: Mobility Header type. - type_default: Default value for Mobility Header type field. - type_namespace: Namespace of Mobility Header type field. - type_reversed: Whether the namespace of Mobility Header type field is reversed. reserved: Reserved octet after the MH type. chksum: Checksum. data: Message data. @@ -1473,7 +1475,8 @@ def make(self, :class:`~pcapkit.protocols.data.internet.mh.MH` data model or a :class:`~pcapkit.protocols.schema.internet.mh.Packet` schema; or if ``reserved``, or the header length of the message, does - not fit in one octet. + not fit in one octet; or if ``next`` or ``type`` is a name, a + :obj:`bool` or a code its field cannot carry. .. versionchanged:: 1.5.0 @@ -1481,10 +1484,8 @@ def make(self, every later positional argument. """ - next_val = self._make_index(next, next_default, namespace=next_namespace, - reversed=next_reversed, pack=False) - type_val = self._make_index(type, type_default, namespace=type_namespace, - reversed=type_reversed, pack=False) + next_val = self._make_index(next, Enum_TransType, bits=8, name='next header') + type_val = self._make_index(type, Enum_Packet, bits=8, name='MH type') if isinstance(data, bytes): data_val = data # type: bytes | Schema_Packet @@ -6776,7 +6777,7 @@ def _make_msg_unknown(self, message: 'Optional[Data_UnknownMessage]' = None, *, def _make_msg_brr(self, message: 'Optional[Data_BindingRefreshRequestMessage]' = None, *, msg_reserved: 'int' = 0, - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long **kwargs: 'Any') -> 'Schema_BindingRefreshRequestMessage': """Make MH binding refresh request (BRR) message type. @@ -6806,7 +6807,7 @@ def _make_msg_brr(self, message: 'Optional[Data_BindingRefreshRequestMessage]' = def _make_msg_hoti(self, message: 'Optional[Data_HomeTestInitMessage]' = None, *, cookie: 'bytes' = b'\x00\x00\x00\x00\x00\x00\x00\x00', - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long msg_reserved: 'bytes' = b'\x00\x00', **kwargs: 'Any') -> 'Schema_HomeTestInitMessage': """Make MH home test init (HoTI) message type. @@ -6838,7 +6839,7 @@ def _make_msg_hoti(self, message: 'Optional[Data_HomeTestInitMessage]' = None, * def _make_msg_coti(self, message: 'Optional[Data_CareofTestInitMessage]' = None, *, cookie: 'bytes' = b'\x00\x00\x00\x00\x00\x00\x00\x00', - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long msg_reserved: 'bytes' = b'\x00\x00', **kwargs: 'Any') -> 'Schema_CareofTestInitMessage': """Make MH care-of test init (CoTI) message type. @@ -6872,7 +6873,7 @@ def _make_msg_hot(self, message: 'Optional[Data_HomeTestMessage]' = None, *, nonce_index: 'int' = 0, cookie: 'bytes' = b'\x00\x00\x00\x00\x00\x00\x00\x00', token: 'bytes' = b'\x00\x00\x00\x00\x00\x00\x00\x00', - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long **kwargs: 'Any') -> 'Schema_HomeTestMessage': """Make MH home test (HoT) message type. @@ -6910,7 +6911,7 @@ def _make_msg_cot(self, message: 'Optional[Data_CareofTestMessage]' = None, *, nonce_index: 'int' = 0, cookie: 'bytes' = b'\x00\x00\x00\x00\x00\x00\x00\x00', token: 'bytes' = b'\x00\x00\x00\x00\x00\x00\x00\x00', - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long **kwargs: 'Any') -> 'Schema_CareofTestMessage': """Make MH care-of test (CoT) message type. @@ -6960,7 +6961,7 @@ def _make_msg_bu(self, message: 'Optional[Data_BindingUpdateMessage]' = None, *, dmm: 'bool' = False, flags_reserved: 'int' = 0, lifetime: 'int | timedelta' = 4, # reasonable default value - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long **kwargs: 'Any') -> 'Schema_BindingUpdateMessage': """Make MH binding update (BU) message type. @@ -7041,10 +7042,7 @@ def _make_msg_bu(self, message: 'Optional[Data_BindingUpdateMessage]' = None, *, ) def _make_msg_ba(self, message: 'Optional[Data_BindingAcknowledgementMessage]' = None, *, - status: 'Enum_StatusCode | StdlibEnum | AenumEnum | str | int' = Enum_StatusCode.Binding_Update_accepted_Proxy_Binding_Update_accepted, - status_default: 'Optional[int]' = None, - status_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - status_reversed: 'bool' = False, + status: 'Enum_StatusCode | int' = Enum_StatusCode.Binding_Update_accepted_Proxy_Binding_Update_accepted, # pylint: disable=line-too-long key_mngt: 'bool' = False, mobile_router: 'bool' = False, proxy_reg: 'bool' = False, @@ -7055,16 +7053,13 @@ def _make_msg_ba(self, message: 'Optional[Data_BindingAcknowledgementMessage]' = flags_reserved: 'int' = 0, seq: 'int' = 0, lifetime: 'int | timedelta' = 4, # reasonable default value - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long **kwargs: 'Any') -> 'Schema_BindingAcknowledgementMessage': """Make MH binding acknowledge (BA) message type. Args: message: Message data model. status: Status code. - status_default: Default status code. - status_namespace: Status code namespace. - status_reversed: Reverse status code namespace. key_mngt: Key management mobility option flag. mobile_router: Mobile router flag (``R``). proxy_reg: Proxy registration flag (``P``). @@ -7084,7 +7079,8 @@ def _make_msg_ba(self, message: 'Optional[Data_BindingAcknowledgementMessage]' = Raises: ProtocolError: If ``seq``, ``flags_reserved`` or the lifetime does not fit - its field. + its field, or ``status`` is a name, a :obj:`bool` or a code its + field cannot carry. """ if message is not None: @@ -7101,8 +7097,7 @@ def _make_msg_ba(self, message: 'Optional[Data_BindingAcknowledgementMessage]' = lifetime_val = math.ceil(message.lifetime.total_seconds()) options = message.options else: - status_val = self._make_index(status, status_default, namespace=status_namespace, # type: ignore[assignment] - reversed=status_reversed, pack=False) + status_val = self._make_index(status, Enum_StatusCode, bits=8, name='status') lifetime_val = lifetime if isinstance(lifetime, int) else math.ceil(lifetime.total_seconds()) options = options or [] @@ -7129,22 +7124,17 @@ def _make_msg_ba(self, message: 'Optional[Data_BindingAcknowledgementMessage]' = ) def _make_msg_be(self, message: 'Optional[Data_BindingErrorMessage]' = None, *, - status: 'Enum_StatusCode | StdlibEnum | AenumEnum | str | int' = Enum_StatusCode.Binding_Update_accepted_Proxy_Binding_Update_accepted, - status_default: 'Optional[int]' = None, - status_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - status_reversed: 'bool' = False, + status: 'Enum_BindingError | int' = 0, home: 'IPv6Address | int | str | bytes' = '::', - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long msg_reserved: 'bytes' = b'\x00', **kwargs: 'Any') -> 'Schema_BindingErrorMessage': """Make MH binding error (BE) message type. Args: message: Message data model. - status: Status code. - status_default: Default status code. - status_namespace: Status code namespace. - status_reversed: Reverse status code namespace. + status: Status [:rfc:`6275#section-6.1.9`]; ``0`` by default, a + code no status is assigned. home: Home address. options: Mobility options. msg_reserved: The reserved octet of the message body as captured, @@ -7154,6 +7144,10 @@ def _make_msg_be(self, message: 'Optional[Data_BindingErrorMessage]' = None, *, Returns: Constructed message type. + Raises: + ProtocolError: If ``status`` is a name, a :obj:`bool` or a code its + field cannot carry. + """ if message is not None: msg_reserved = getattr(message, 'msg_reserved', b'\x00') @@ -7161,8 +7155,7 @@ def _make_msg_be(self, message: 'Optional[Data_BindingErrorMessage]' = None, *, home = message.home options = message.options else: - status_val = self._make_index(status, status_default, namespace=status_namespace, # type: ignore[assignment] - reversed=status_reversed, pack=False) + status_val = self._make_index(status, Enum_BindingError, bits=8, name='status') options = options or [] return Schema_BindingErrorMessage( @@ -7179,7 +7172,7 @@ def _make_msg_fbu(self, message: 'Optional[Data_FastBindingUpdateMessage]' = Non lla_compat: 'bool' = False, key_mngt: 'bool' = False, lifetime: 'int | timedelta' = 4, # reasonable default value - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long flags_reserved: 'int' = 0, **kwargs: 'Any') -> 'Schema_FastBindingUpdateMessage': """Make MH fast binding update (FBU) message type. @@ -7234,14 +7227,11 @@ def _make_msg_fbu(self, message: 'Optional[Data_FastBindingUpdateMessage]' = Non ) def _make_msg_fback(self, message: 'Optional[Data_FastBindingAcknowledgmentMessage]' = None, *, - status: 'FastBindingAcknowledgmentStatus | StdlibEnum | AenumEnum | str | int' = FastBindingAcknowledgmentStatus.Fast_Binding_Update_accepted, - status_default: 'Optional[int]' = None, - status_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - status_reversed: 'bool' = False, + status: 'FastBindingAcknowledgmentStatus | int' = FastBindingAcknowledgmentStatus.Fast_Binding_Update_accepted, # pylint: disable=line-too-long key_mngt: 'bool' = False, seq: 'int' = 0, lifetime: 'int | timedelta' = 4, # reasonable default value - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long flags_reserved: 'int' = 0, **kwargs: 'Any') -> 'Schema_FastBindingAcknowledgmentMessage': """Make MH fast binding acknowledgment (FBack) message type. @@ -7250,9 +7240,6 @@ def _make_msg_fback(self, message: 'Optional[Data_FastBindingAcknowledgmentMessa message: Message data model. status: Status code, see :class:`~pcapkit.protocols.internet.mh.FastBindingAcknowledgmentStatus`. - status_default: Default status code. - status_namespace: Status code namespace. - status_reversed: Reverse status code namespace. key_mngt: Key management mobility option flag. seq: Sequence number. lifetime: Lifetime, in seconds or as a @@ -7268,7 +7255,8 @@ def _make_msg_fback(self, message: 'Optional[Data_FastBindingAcknowledgmentMessa Raises: ProtocolError: If ``seq``, ``flags_reserved`` or the lifetime does not fit - its field. + its field, or ``status`` is a name, a :obj:`bool` or a code its + field cannot carry. """ if message is not None: @@ -7279,8 +7267,8 @@ def _make_msg_fback(self, message: 'Optional[Data_FastBindingAcknowledgmentMessa lifetime_val = math.ceil(message.lifetime.total_seconds()) options = message.options else: - status_val = self._make_index(status, status_default, namespace=status_namespace, # type: ignore[assignment] - reversed=status_reversed, pack=False) + status_val = self._make_index(status, FastBindingAcknowledgmentStatus, bits=8, + name='status') lifetime_val = lifetime if isinstance(lifetime, int) else math.ceil(lifetime.total_seconds()) options = options or [] @@ -7296,7 +7284,7 @@ def _make_msg_fback(self, message: 'Optional[Data_FastBindingAcknowledgmentMessa ) def _make_msg_fna(self, message: 'Optional[Data_FastNeighborAdvertisementMessage]' = None, *, - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long msg_reserved: 'bytes' = b'\x00\x00', **kwargs: 'Any') -> 'Schema_FastNeighborAdvertisementMessage': """Make MH fast neighbor advertisement (FNA) message type. @@ -7350,11 +7338,8 @@ def _make_msg_hi(self, message: 'Optional[Data_HandoverInitiateMessage]' = None, buffer: 'bool' = False, proxy: 'bool' = False, forward: 'bool' = False, - code: 'Enum_HandoverInitiateStatus | StdlibEnum | AenumEnum | str | int' = Enum_HandoverInitiateStatus.FBU_with_the_PCoA_as_source_IP_address, - code_default: 'Optional[int]' = None, - code_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - code_reversed: 'bool' = False, - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + code: 'Enum_HandoverInitiateStatus | int' = Enum_HandoverInitiateStatus.FBU_with_the_PCoA_as_source_IP_address, # pylint: disable=line-too-long + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long flags_reserved: 'int' = 0, **kwargs: 'Any') -> 'Schema_HandoverInitiateMessage': """Make MH handover initiate (HI) message type. @@ -7367,9 +7352,6 @@ def _make_msg_hi(self, message: 'Optional[Data_HandoverInitiateMessage]' = None, proxy: Proxy flag. forward: Forwarding flag. code: Code. - code_default: Default code. - code_namespace: Code namespace. - code_reversed: Reverse code namespace. options: Mobility options. flags_reserved: Reserved bits, i.e. the four bits after the ``F`` flag. **kwargs: Arbitrary keyword arguments. @@ -7378,7 +7360,9 @@ def _make_msg_hi(self, message: 'Optional[Data_HandoverInitiateMessage]' = None, Constructed message type. Raises: - ProtocolError: If ``seq`` or ``flags_reserved`` does not fit its field. + ProtocolError: If ``seq`` or ``flags_reserved`` does not fit its field, + or ``code`` is a name, a :obj:`bool` or a code its field cannot + carry. """ if message is not None: @@ -7391,8 +7375,7 @@ def _make_msg_hi(self, message: 'Optional[Data_HandoverInitiateMessage]' = None, code_val = message.code options = message.options else: - code_val = self._make_index(code, code_default, namespace=code_namespace, # type: ignore[assignment] - reversed=code_reversed, pack=False) + code_val = self._make_index(code, Enum_HandoverInitiateStatus, bits=8, name='code') options = options or [] return Schema_HandoverInitiateMessage( @@ -7413,11 +7396,8 @@ def _make_msg_hack(self, message: 'Optional[Data_HandoverAcknowledgeMessage]' = buffer: 'bool' = False, proxy: 'bool' = False, forward: 'bool' = False, - code: 'Enum_HandoverACKStatus | StdlibEnum | AenumEnum | str | int' = Enum_HandoverACKStatus.Handover_Accepted_with_NCoA_valid, - code_default: 'Optional[int]' = None, - code_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - code_reversed: 'bool' = False, - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + code: 'Enum_HandoverACKStatus | int' = Enum_HandoverACKStatus.Handover_Accepted_with_NCoA_valid, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long flags_reserved: 'int' = 0, **kwargs: 'Any') -> 'Schema_HandoverAcknowledgeMessage': """Make MH handover acknowledge (HAck) message type. @@ -7429,9 +7409,6 @@ def _make_msg_hack(self, message: 'Optional[Data_HandoverAcknowledgeMessage]' = proxy: Proxy flag. forward: Forwarding flag. code: Code. - code_default: Default code. - code_namespace: Code namespace. - code_reversed: Reverse code namespace. options: Mobility options. flags_reserved: Reserved bits, i.e. the five bits after the ``F`` flag. **kwargs: Arbitrary keyword arguments. @@ -7440,7 +7417,9 @@ def _make_msg_hack(self, message: 'Optional[Data_HandoverAcknowledgeMessage]' = Constructed message type. Raises: - ProtocolError: If ``seq`` or ``flags_reserved`` does not fit its field. + ProtocolError: If ``seq`` or ``flags_reserved`` does not fit its field, + or ``code`` is a name, a :obj:`bool` or a code its field cannot + carry. """ if message is not None: @@ -7452,8 +7431,7 @@ def _make_msg_hack(self, message: 'Optional[Data_HandoverAcknowledgeMessage]' = code_val = message.code options = message.options else: - code_val = self._make_index(code, code_default, namespace=code_namespace, # type: ignore[assignment] - reversed=code_reversed, pack=False) + code_val = self._make_index(code, Enum_HandoverACKStatus, bits=8, name='code') options = options or [] return Schema_HandoverAcknowledgeMessage( @@ -7470,7 +7448,7 @@ def _make_msg_hack(self, message: 'Optional[Data_HandoverAcknowledgeMessage]' = def _make_msg_has(self, message: 'Optional[Data_HomeAgentSwitchMessage]' = None, *, addresses: 'Optional[list[IPv6Address | bytes | str | int]]' = None, - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long msg_reserved: 'bytes' = b'\x00', **kwargs: 'Any') -> 'Schema_HomeAgentSwitchMessage': """Make MH home agent switch (HAS) message type. @@ -7507,7 +7485,7 @@ def _make_msg_hb(self, message: 'Optional[Data_HeartbeatMessage]' = None, *, unsolicited: 'bool' = False, response: 'bool' = False, seq: 'int' = 0, - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long flags_reserved: 'int' = 0, **kwargs: 'Any') -> 'Schema_HeartbeatMessage': """Make MH heartbeat message type. @@ -7548,19 +7526,13 @@ def _make_msg_hb(self, message: 'Optional[Data_HeartbeatMessage]' = None, *, ) def _make_msg_brm(self, message: 'Optional[Data_BindingRevocationMessage]' = None, *, - br_type: 'Enum_BindingRevocation | StdlibEnum | AenumEnum | str | int' = Enum_BindingRevocation.Binding_Revocation_Indication, - br_type_default: 'Optional[int]' = None, - br_type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - br_type_reversed: 'bool' = False, - code: 'Enum_RevocationTrigger | Enum_RevocationStatusCode | StdlibEnum | AenumEnum | str | int' = Enum_RevocationTrigger.Unspecified, - code_default: 'Optional[int]' = None, - code_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - code_reversed: 'bool' = False, + br_type: 'Enum_BindingRevocation | int' = Enum_BindingRevocation.Binding_Revocation_Indication, + code: 'Enum_RevocationTrigger | Enum_RevocationStatusCode | int' = Enum_RevocationTrigger.Unspecified, # pylint: disable=line-too-long seq: 'int' = 0, proxy: 'bool' = False, ipv4_hoa: 'bool' = False, global_revocation: 'bool' = False, - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long flags_reserved: 'int' = 0, **kwargs: 'Any') -> 'Schema_BindingRevocationMessage': """Make MH binding revocation (BRM) message type. @@ -7569,14 +7541,9 @@ def _make_msg_brm(self, message: 'Optional[Data_BindingRevocationMessage]' = Non message: Message data model. br_type: Binding revocation type, saying which form of the message this is. - br_type_default: Default binding revocation type. - br_type_namespace: Binding revocation type namespace. - br_type_reversed: Reverse binding revocation type namespace. code: Revocation trigger, for an indication, or acknowledgement - status, for an acknowledgement. - code_default: Default code. - code_namespace: Code namespace. - code_reversed: Reverse code namespace. + status, for an acknowledgement; resolved in the registry of the + form ``br_type`` names. seq: Sequence number. proxy: Proxy binding flag. ipv4_hoa: IPv4 home address binding only flag. @@ -7589,7 +7556,9 @@ def _make_msg_brm(self, message: 'Optional[Data_BindingRevocationMessage]' = Non Constructed message type. Raises: - ProtocolError: If ``seq`` or ``flags_reserved`` does not fit its field. + ProtocolError: If ``seq`` or ``flags_reserved`` does not fit its field, + or ``br_type`` or ``code`` is a name, a :obj:`bool` or a code its + field cannot carry. """ if message is not None: @@ -7602,10 +7571,14 @@ def _make_msg_brm(self, message: 'Optional[Data_BindingRevocationMessage]' = Non flags_reserved = message.flags_reserved options = message.options else: - br_type_val = self._make_index(br_type, br_type_default, namespace=br_type_namespace, # type: ignore[assignment] - reversed=br_type_reversed, pack=False) - code_val = self._make_index(code, code_default, namespace=code_namespace, # type: ignore[assignment] - reversed=code_reversed, pack=False) + br_type_val = self._make_index(br_type, Enum_BindingRevocation, bits=8, + name='binding revocation type') + # NOTE: The registry ``br_code_selector`` reads the octet from. + if br_type_val == Enum_BindingRevocation.Binding_Revocation_Acknowledgement: + code_val = self._make_index(code, Enum_RevocationStatusCode, bits=8, name='status') + else: + code_val = self._make_index(code, Enum_RevocationTrigger, bits=8, + name='revocation trigger') options = options or [] return Schema_BindingRevocationMessage( @@ -7624,7 +7597,7 @@ def _make_msg_brm(self, message: 'Optional[Data_BindingRevocationMessage]' = Non def _make_msg_lri(self, message: 'Optional[Data_LocalizedRoutingInitiationMessage]' = None, *, seq: 'int' = 0, lifetime: 'int | timedelta' = 0, - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long msg_reserved: 'bytes' = b'\x00\x00', **kwargs: 'Any') -> 'Schema_LocalizedRoutingInitiationMessage': """Make MH localized routing initiation (LRI) message type. @@ -7667,12 +7640,9 @@ def _make_msg_lri(self, message: 'Optional[Data_LocalizedRoutingInitiationMessag def _make_msg_lra(self, message: 'Optional[Data_LocalizedRoutingAcknowledgmentMessage]' = None, *, seq: 'int' = 0, unsolicited: 'bool' = False, - status: 'LocalizedRoutingStatus | StdlibEnum | AenumEnum | str | int' = LocalizedRoutingStatus.Success, - status_default: 'Optional[int]' = None, - status_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - status_reversed: 'bool' = False, + status: 'LocalizedRoutingStatus | int' = LocalizedRoutingStatus.Success, lifetime: 'int | timedelta' = 0, - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long flags_reserved: 'int' = 0, **kwargs: 'Any') -> 'Schema_LocalizedRoutingAcknowledgmentMessage': """Make MH localized routing acknowledgment (LRA) message type. @@ -7683,9 +7653,6 @@ def _make_msg_lra(self, message: 'Optional[Data_LocalizedRoutingAcknowledgmentMe unsolicited: Unsolicited flag. status: Status, see :class:`~pcapkit.protocols.internet.mh.LocalizedRoutingStatus`. - status_default: Default status. - status_namespace: Status namespace. - status_reversed: Reverse status namespace. lifetime: Lifetime, in seconds or as a :class:`~datetime.timedelta`. options: Mobility options. @@ -7697,7 +7664,8 @@ def _make_msg_lra(self, message: 'Optional[Data_LocalizedRoutingAcknowledgmentMe Raises: ProtocolError: If ``seq``, ``flags_reserved`` or the lifetime does not fit - its field. + its field, or ``status`` is a name, a :obj:`bool` or a code its + field cannot carry. """ if message is not None: @@ -7708,8 +7676,7 @@ def _make_msg_lra(self, message: 'Optional[Data_LocalizedRoutingAcknowledgmentMe lifetime_val = math.ceil(message.lifetime.total_seconds()) options = message.options else: - status_val = self._make_index(status, status_default, namespace=status_namespace, - reversed=status_reversed, pack=False) + status_val = self._make_index(status, LocalizedRoutingStatus, bits=8, name='status') lifetime_val = lifetime if isinstance(lifetime, int) else math.ceil(lifetime.total_seconds()) options = options or [] @@ -7726,13 +7693,10 @@ def _make_msg_lra(self, message: 'Optional[Data_LocalizedRoutingAcknowledgmentMe def _make_msg_upn(self, message: 'Optional[Data_UpdateNotificationMessage]' = None, *, seq: 'int' = 0, - reason: 'Enum_UpdateNotificationReason | StdlibEnum | AenumEnum | str | int' = Enum_UpdateNotificationReason.FORCE_REREGISTRATION, - reason_default: 'Optional[int]' = None, - reason_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - reason_reversed: 'bool' = False, + reason: 'Enum_UpdateNotificationReason | int' = Enum_UpdateNotificationReason.FORCE_REREGISTRATION, # pylint: disable=line-too-long ack: 'bool' = False, retransmit: 'bool' = False, - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long flags_reserved: 'int' = 0, **kwargs: 'Any') -> 'Schema_UpdateNotificationMessage': """Make MH update notification (UPN) message type. @@ -7741,9 +7705,6 @@ def _make_msg_upn(self, message: 'Optional[Data_UpdateNotificationMessage]' = No message: Message data model. seq: Sequence number. reason: Notification reason. - reason_default: Default notification reason. - reason_namespace: Notification reason namespace. - reason_reversed: Reverse notification reason namespace. ack: Acknowledgement requested flag. retransmit: Retransmission flag. options: Mobility options. @@ -7754,7 +7715,9 @@ def _make_msg_upn(self, message: 'Optional[Data_UpdateNotificationMessage]' = No Constructed message type. Raises: - ProtocolError: If ``seq`` or ``flags_reserved`` does not fit its field. + ProtocolError: If ``seq`` or ``flags_reserved`` does not fit its field, + or ``reason`` is a name, a :obj:`bool` or a code its field cannot + carry. """ if message is not None: @@ -7765,8 +7728,8 @@ def _make_msg_upn(self, message: 'Optional[Data_UpdateNotificationMessage]' = No flags_reserved = message.flags_reserved options = message.options else: - reason_val = self._make_index(reason, reason_default, namespace=reason_namespace, # type: ignore[assignment] - reversed=reason_reversed, pack=False) + reason_val = self._make_index(reason, Enum_UpdateNotificationReason, bits=16, + name='notification reason') options = options or [] return Schema_UpdateNotificationMessage( @@ -7782,11 +7745,8 @@ def _make_msg_upn(self, message: 'Optional[Data_UpdateNotificationMessage]' = No def _make_msg_upa(self, message: 'Optional[Data_UpdateNotificationAcknowledgementMessage]' = None, *, seq: 'int' = 0, - status: 'Enum_UpdateNotificationACKStatus | StdlibEnum | AenumEnum | str | int' = Enum_UpdateNotificationACKStatus.SUCCESS, - status_default: 'Optional[int]' = None, - status_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - status_reversed: 'bool' = False, - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + status: 'Enum_UpdateNotificationACKStatus | int' = Enum_UpdateNotificationACKStatus.SUCCESS, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long msg_reserved: 'bytes' = b'\x00\x00\x00', **kwargs: 'Any') -> 'Schema_UpdateNotificationAcknowledgementMessage': """Make MH update notification acknowledgement (UPA) message type. @@ -7795,9 +7755,6 @@ def _make_msg_upa(self, message: 'Optional[Data_UpdateNotificationAcknowledgemen message: Message data model. seq: Sequence number. status: Status. - status_default: Default status. - status_namespace: Status namespace. - status_reversed: Reverse status namespace. options: Mobility options. msg_reserved: The three reserved octets of the message body as captured, truncated or zero-filled to 3 octets. @@ -7807,7 +7764,8 @@ def _make_msg_upa(self, message: 'Optional[Data_UpdateNotificationAcknowledgemen Constructed message type. Raises: - ProtocolError: If ``seq`` does not fit its field. + ProtocolError: If ``seq`` does not fit its field, or ``status`` is a + name, a :obj:`bool` or a code its field cannot carry. """ if message is not None: @@ -7816,8 +7774,8 @@ def _make_msg_upa(self, message: 'Optional[Data_UpdateNotificationAcknowledgemen status_val = message.status # type: Enum_UpdateNotificationACKStatus options = message.options else: - status_val = self._make_index(status, status_default, namespace=status_namespace, # type: ignore[assignment] - reversed=status_reversed, pack=False) + status_val = self._make_index(status, Enum_UpdateNotificationACKStatus, bits=8, + name='status') options = options or [] return Schema_UpdateNotificationAcknowledgementMessage( @@ -7828,17 +7786,11 @@ def _make_msg_upa(self, message: 'Optional[Data_UpdateNotificationAcknowledgemen ) def _make_msg_fbm(self, message: 'Optional[Data_FlowBindingMessage]' = None, *, - fb_type: 'Enum_FlowBindingType | StdlibEnum | AenumEnum | str | int' = Enum_FlowBindingType.Indication, - fb_type_default: 'Optional[int]' = None, - fb_type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - fb_type_reversed: 'bool' = False, + fb_type: 'Enum_FlowBindingType | int' = Enum_FlowBindingType.Indication, seq: 'int' = 0, - code: 'Enum_FlowBindingIndicationTrigger | Enum_FlowBindingACKStatus | StdlibEnum | AenumEnum | str | int' = Enum_FlowBindingIndicationTrigger.Unspecified, - code_default: 'Optional[int]' = None, - code_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - code_reversed: 'bool' = False, + code: 'Enum_FlowBindingIndicationTrigger | Enum_FlowBindingACKStatus | int' = Enum_FlowBindingIndicationTrigger.Unspecified, # pylint: disable=line-too-long ack: 'bool' = False, - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long flags_reserved: 'int' = 0, **kwargs: 'Any') -> 'Schema_FlowBindingMessage': """Make MH flow binding (FB) message type. @@ -7846,15 +7798,10 @@ def _make_msg_fbm(self, message: 'Optional[Data_FlowBindingMessage]' = None, *, Args: message: Message data model. fb_type: Flow binding type, saying which form of the message this is. - fb_type_default: Default flow binding type. - fb_type_namespace: Flow binding type namespace. - fb_type_reversed: Reverse flow binding type namespace. seq: Sequence number. code: Indication trigger, for an indication, or acknowledgement - status, for an acknowledgement. - code_default: Default code. - code_namespace: Code namespace. - code_reversed: Reverse code namespace. + status, for an acknowledgement; resolved in the registry of the + form ``fb_type`` names. ack: Acknowledgement requested flag. Meaningful only in an indication. options: Mobility options. @@ -7865,7 +7812,9 @@ def _make_msg_fbm(self, message: 'Optional[Data_FlowBindingMessage]' = None, *, Constructed message type. Raises: - ProtocolError: If ``seq`` or ``flags_reserved`` does not fit its field. + ProtocolError: If ``seq`` or ``flags_reserved`` does not fit its field, + or ``fb_type`` or ``code`` is a name, a :obj:`bool` or a code its + field cannot carry. """ if message is not None: @@ -7876,10 +7825,14 @@ def _make_msg_fbm(self, message: 'Optional[Data_FlowBindingMessage]' = None, *, flags_reserved = message.flags_reserved options = message.options else: - fb_type_val = self._make_index(fb_type, fb_type_default, namespace=fb_type_namespace, # type: ignore[assignment] - reversed=fb_type_reversed, pack=False) - code_val = self._make_index(code, code_default, namespace=code_namespace, # type: ignore[assignment] - reversed=code_reversed, pack=False) + fb_type_val = self._make_index(fb_type, Enum_FlowBindingType, bits=16, + name='flow binding type') + # NOTE: The registry ``fb_code_selector`` reads the octet from. + if fb_type_val == Enum_FlowBindingType.Acknowledgement: + code_val = self._make_index(code, Enum_FlowBindingACKStatus, bits=8, name='status') + else: + code_val = self._make_index(code, Enum_FlowBindingIndicationTrigger, bits=8, + name='trigger') options = options or [] return Schema_FlowBindingMessage( @@ -7895,7 +7848,7 @@ def _make_msg_fbm(self, message: 'Optional[Data_FlowBindingMessage]' = None, *, def _make_msg_sq(self, message: 'Optional[Data_SubscriptionQueryMessage]' = None, *, seq: 'int' = 0, - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long msg_reserved: 'bytes' = b'\x00', **kwargs: 'Any') -> 'Schema_SubscriptionQueryMessage': """Make MH subscription query (SQ) message type. @@ -7931,7 +7884,7 @@ def _make_msg_sq(self, message: 'Optional[Data_SubscriptionQueryMessage]' = None def _make_msg_sr(self, message: 'Optional[Data_SubscriptionResponseMessage]' = None, *, seq: 'int' = 0, info: 'bool' = False, - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long flags_reserved: 'int' = 0, **kwargs: 'Any') -> 'Schema_SubscriptionResponseMessage': """Make MH subscription response (SR) message type. @@ -8007,7 +7960,7 @@ def _make_pad_options(self, offset: 'int') -> 'tuple[list[Schema_PadOption], int return [self._make_opt_pad(Enum_Option.Pad1, length=0)], pad_len # type: ignore[arg-type] return [self._make_opt_pad(Enum_Option.PadN, length=pad_len - 2)], pad_len # type: ignore[arg-type] - def _make_mh_options(self, options: 'Option | list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes]') -> 'list[Schema_Option | bytes]': + def _make_mh_options(self, options: 'Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]') -> 'list[Schema_Option | bytes]': # pylint: disable=line-too-long """Make options for MH. Args: @@ -8016,6 +7969,11 @@ def _make_mh_options(self, options: 'Option | list[Schema_Option | tuple[Enum_Op Returns: Mobility options list. + Raises: + ProtocolError: If the type of an option, given in a ``(type, + kwargs)`` pair or as the key of parsed data, is a name, a + :obj:`bool` or a code its field cannot carry. + """ if isinstance(options, list): options_list = [] # type: list[Schema_Option | bytes] @@ -8027,7 +7985,8 @@ def _make_mh_options(self, options: 'Option | list[Schema_Option | tuple[Enum_Op elif isinstance(schema, Schema): data = schema else: - code, args = cast('tuple[Enum_Option, dict[str, Any]]', schema) + kind, args = cast('tuple[Enum_Option | int, dict[str, Any]]', schema) + code = self._make_index(kind, Enum_Option, bits=8, name='option type') name = self._lookup_registry(self.__option__, code) if isinstance(name, str): meth_name = f'_make_opt_{name}' @@ -8041,7 +8000,8 @@ def _make_mh_options(self, options: 'Option | list[Schema_Option | tuple[Enum_Op return options_list options_list = [] - for code, option in options.items(multi=True): + for key, option in options.items(multi=True): + code = self._make_index(key, Enum_Option, bits=8, name='option type') name = self._lookup_registry(self.__option__, code) if isinstance(name, str): meth_name = f'_make_opt_{name}' @@ -8323,10 +8283,7 @@ def _make_opt_lla(self, type: 'Enum_Option', option: 'Optional[Data_LinkLayerAdd ) def _make_opt_mn_id(self, type: 'Enum_Option', option: 'Optional[Data_MNIDOption]' = None, *, - subtype: 'Enum_MNIDSubtype | StdlibEnum | AenumEnum | str | int' = Enum_MNIDSubtype.IPv6_Address, - subtype_default: 'Optional[int]' = None, - subtype_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - subtype_reversed: 'bool' = False, + subtype: 'Enum_MNIDSubtype | int' = Enum_MNIDSubtype.IPv6_Address, identifier: 'bytes | str | IPv6Address | int' = '::', **kwargs: 'Any') -> 'Schema_MNIDOption': """Make MH mobile node identifier option. @@ -8335,9 +8292,6 @@ def _make_opt_mn_id(self, type: 'Enum_Option', option: 'Optional[Data_MNIDOption type: Option type. option: Option data model. subtype: MN-ID subtype. - subtype_default: MN-ID subtype default value. - subtype_namespace: MN-ID subtype namespace. - subtype_reversed: MN-ID subtype reversed flag. identifier: Identifier. An :obj:`int` is accepted for every subtype except ``NAI``, with the sole exception of a :obj:`bool`, which is rejected for every subtype -- see @@ -8374,8 +8328,9 @@ def _make_opt_mn_id(self, type: 'Enum_Option', option: 'Optional[Data_MNIDOption subtypes -- an :obj:`int` is converted rather than rejected there -- or anything :class:`ipaddress.IPv6Address` itself does not accept for ``IPv6_Address``. - Also if ``subtype`` does not fit in one octet, or the option is too long - for its one-octet ``Option Length``. + Also if ``subtype`` is a name, a :obj:`bool` or a code its field + cannot carry, or the option is too long for its one-octet + ``Option Length``. .. versionchanged:: 1.6.0 @@ -8386,9 +8341,9 @@ def _make_opt_mn_id(self, type: 'Enum_Option', option: 'Optional[Data_MNIDOption if option is not None: subtype_val = option.subtype identifier = option.identifier + self._make_uint(subtype_val, bits=8, name='MN-ID subtype') else: - subtype_val = self._make_index(subtype, subtype_default, namespace=subtype_namespace, # type: ignore[assignment] - reversed=subtype_reversed, pack=False) + subtype_val = self._make_index(subtype, Enum_MNIDSubtype, bits=8, name='MN-ID subtype') if isinstance(identifier, int) and identifier < 0: # NOTE: checked before the subtype dispatch below, not inside it, @@ -8400,19 +8355,8 @@ def _make_opt_mn_id(self, type: 'Enum_Option', option: 'Optional[Data_MNIDOption # guard living inside the ``elif isinstance(identifier, int)`` branch # could not catch, since the ``IPv6_Address`` dispatch never reaches # it. - try: - # ``Enum_MNIDSubtype(subtype_val)`` round-trips a plain int back - # into a named member for the message below -- but its own - # ``_missing_`` only auto-extends 9-15 and 16-255, so 0, - # negatives and anything above 255 make the constructor itself - # raise a bare ``ValueError``, which would defeat the point of - # this guard. Caught here and the raw value used instead - # rather than let it propagate. - subtype_repr = repr(Enum_MNIDSubtype(subtype_val)) - except ValueError: - subtype_repr = repr(subtype_val) raise ProtocolError( - f'{self.alias}: [OptNo {type}] MN-ID subtype {subtype_repr} ' + f'{self.alias}: [OptNo {type}] MN-ID subtype {subtype_val!r} ' f'identifier must be a non-negative int, not {identifier!r}') if isinstance(identifier, bool): @@ -8531,16 +8475,11 @@ def _make_opt_mn_id(self, type: 'Enum_Option', option: 'Optional[Data_MNIDOption # ``struct.error`` as str/list/dict), so it is rejected with # everything else rather than let through to leak anyway. if not isinstance(identifier, (bytes, bytearray)): - try: - subtype_repr = repr(Enum_MNIDSubtype(subtype_val)) - except ValueError: - subtype_repr = repr(subtype_val) raise ProtocolError( - f'{self.alias}: [OptNo {type}] MN-ID subtype {subtype_repr} ' + f'{self.alias}: [OptNo {type}] MN-ID subtype {subtype_val!r} ' f'identifier must be bytes, not {identifier!r}') id_len = len(identifier) - self._make_uint(subtype_val, bits=8, name='MN-ID subtype') return Schema_MNIDOption( type=type, length=self._make_uint(1 + id_len, bits=8, name='Option Length'), @@ -8549,10 +8488,7 @@ def _make_opt_mn_id(self, type: 'Enum_Option', option: 'Optional[Data_MNIDOption ) def _make_opt_auth(self, type: 'Enum_Option', option: 'Optional[Data_AuthOption]' = None, *, - subtype: 'Enum_AuthSubtype | StdlibEnum | AenumEnum | str | int' = Enum_AuthSubtype.MN_HA, - subtype_default: 'Optional[int]' = None, - subtype_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - subtype_reversed: 'bool' = False, + subtype: 'Enum_AuthSubtype | int' = Enum_AuthSubtype.MN_HA, spi: 'int' = 0, data: 'bytes' = b'', **kwargs: 'Any') -> 'Schema_AuthOption': @@ -8562,9 +8498,6 @@ def _make_opt_auth(self, type: 'Enum_Option', option: 'Optional[Data_AuthOption] type: Option type. option: Option data model. subtype: Authentication subtype. - subtype_default: Authentication subtype default value. - subtype_namespace: Authentication subtype namespace. - subtype_reversed: Authentication subtype reversed flag. spi: Security parameter index. data: Authentication data. **kwargs: Arbitrary keyword arguments. @@ -8577,7 +8510,9 @@ def _make_opt_auth(self, type: 'Enum_Option', option: 'Optional[Data_AuthOption] four, which is the only length leaving an ``Option Length`` of ``5 + len(data)`` that :meth:`_read_opt_auth` accepts; or if ``spi`` does not fit in four octets. - Also if the option is too long for its one-octet ``Option Length``. + Also if ``subtype`` is a name, a :obj:`bool` or a code its field + cannot carry, or the option is too long for its one-octet + ``Option Length``. """ if option is not None: @@ -8585,8 +8520,7 @@ def _make_opt_auth(self, type: 'Enum_Option', option: 'Optional[Data_AuthOption] spi = option.spi data = option.data else: - subtype_val = self._make_index(subtype, subtype_default, namespace=subtype_namespace, # type: ignore[assignment] - reversed=subtype_reversed, pack=False) + subtype_val = self._make_index(subtype, Enum_AuthSubtype, bits=8, name='subtype') if (len(data) + 6) % 4 != 0: raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid data: must be two octets ' @@ -8885,10 +8819,7 @@ def _make_opt_badf(self, type: 'Enum_Option', option: 'Optional[Data_BADFOption] ) def _make_opt_ipv6_ap(self, type: 'Enum_Option', option: 'Optional[Data_IPv6AddressPrefixOption]' = None, *, - code: 'IPv6AddressPrefixCode | StdlibEnum | AenumEnum | str | int' = IPv6AddressPrefixCode.New_Care_of_Address, - code_default: 'Optional[int]' = None, - code_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - code_reversed: 'bool' = False, + code: 'IPv6AddressPrefixCode | int' = IPv6AddressPrefixCode.New_Care_of_Address, prefix_length: 'int' = 128, address: 'bytes | str | int | IPv6Address' = '::', **kwargs: 'Any') -> 'Schema_IPv6AddressPrefixOption': @@ -8899,9 +8830,6 @@ def _make_opt_ipv6_ap(self, type: 'Enum_Option', option: 'Optional[Data_IPv6Addr option: Option data model. code: Option code, see :class:`~pcapkit.protocols.internet.mh.IPv6AddressPrefixCode`. - code_default: Default option code. - code_namespace: Option code namespace. - code_reversed: Reverse option code namespace. prefix_length: Prefix length. address: IPv6 address/prefix. **kwargs: Arbitrary keyword arguments. @@ -8911,8 +8839,8 @@ def _make_opt_ipv6_ap(self, type: 'Enum_Option', option: 'Optional[Data_IPv6Addr Raises: ProtocolError: If ``prefix_length`` is negative, a :obj:`bool`, - or longer than the address, or ``code`` is an :obj:`int` that - does not fit in one octet. + or longer than the address, or ``code`` is a name, a + :obj:`bool` or a code its field cannot carry. """ if option is not None: @@ -8920,12 +8848,7 @@ def _make_opt_ipv6_ap(self, type: 'Enum_Option', option: 'Optional[Data_IPv6Addr prefix_length = option.prefix_length address = option.address else: - if isinstance(code, int): - # NOTE: Before the lookup, whose strict registry would raise a - # bare EnumValueError naming neither MH nor the field. - self._make_uint(code, bits=8, name='code') - code_val = self._make_index(code, code_default, namespace=code_namespace, # type: ignore[assignment] - reversed=code_reversed, pack=False) + code_val = self._make_index(code, IPv6AddressPrefixCode, bits=8, name='code') prefix_length = self._make_uint(prefix_length, bits=8, name='prefix length') if prefix_length > 128: @@ -8940,10 +8863,7 @@ def _make_opt_ipv6_ap(self, type: 'Enum_Option', option: 'Optional[Data_IPv6Addr ) def _make_opt_dns(self, type: 'Enum_Option', option: 'Optional[Data_DNSUpdateOption]' = None, *, - status: 'Enum_DNSStatusCode | StdlibEnum | AenumEnum | str | int' = Enum_DNSStatusCode.DNS_update_performed, - status_default: 'Optional[int]' = None, - status_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - status_reversed: 'bool' = False, + status: 'Enum_DNSStatusCode | int' = Enum_DNSStatusCode.DNS_update_performed, remove: 'bool' = False, identity: 'bytes' = b'', reserved: 'int' = 0, @@ -8954,9 +8874,6 @@ def _make_opt_dns(self, type: 'Enum_Option', option: 'Optional[Data_DNSUpdateOpt type: Option type. option: Option data model. status: Status. - status_default: Default status. - status_namespace: Status namespace. - status_reversed: Reverse status namespace. remove: Remove flag. identity: Mobile node identity, in FQDN form. reserved: Reserved bits, i.e. the seven bits after the ``R`` flag. @@ -8967,8 +8884,9 @@ def _make_opt_dns(self, type: 'Enum_Option', option: 'Optional[Data_DNSUpdateOpt Raises: ProtocolError: If ``reserved`` does not fit its field. - Also if ``status`` does not fit in one octet, or the option is too long - for its one-octet ``Option Length``. + Also if ``status`` is a name, a :obj:`bool` or a code its field + cannot carry, or the option is too long for its one-octet + ``Option Length``. """ if option is not None: @@ -8976,11 +8894,10 @@ def _make_opt_dns(self, type: 'Enum_Option', option: 'Optional[Data_DNSUpdateOpt remove = option.remove reserved = option.reserved identity = option.identity + self._make_uint(status_val, bits=8, name='status') else: - status_val = self._make_index(status, status_default, namespace=status_namespace, # type: ignore[assignment] - reversed=status_reversed, pack=False) + status_val = self._make_index(status, Enum_DNSStatusCode, bits=8, name='status') - self._make_uint(status_val, bits=8, name='status') return Schema_DNSUpdateOption( type=type, length=self._make_uint(2 + len(identity), bits=8, name='Option Length'), @@ -9109,10 +9026,7 @@ def _make_opt_hnp(self, type: 'Enum_Option', option: 'Optional[Data_HomeNetworkP ) def _make_opt_hi(self, type: 'Enum_Option', option: 'Optional[Data_HandoffIndicatorOption]' = None, *, - hi: 'Enum_HandoffType | StdlibEnum | AenumEnum | str | int' = Enum_HandoffType.Attachment_over_a_new_interface, - hi_default: 'Optional[int]' = None, - hi_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - hi_reversed: 'bool' = False, + hi: 'Enum_HandoffType | int' = Enum_HandoffType.Attachment_over_a_new_interface, reserved: 'int' = 0, **kwargs: 'Any') -> 'Schema_HandoffIndicatorOption': """Make MH handoff indicator option. @@ -9121,9 +9035,6 @@ def _make_opt_hi(self, type: 'Enum_Option', option: 'Optional[Data_HandoffIndica type: Option type. option: Option data model. hi: Handoff indicator. - hi_default: Default handoff indicator. - hi_namespace: Handoff indicator namespace. - hi_reversed: Reverse handoff indicator namespace. reserved: Reserved octet before the handoff indicator. **kwargs: Arbitrary keyword arguments. @@ -9132,17 +9043,17 @@ def _make_opt_hi(self, type: 'Enum_Option', option: 'Optional[Data_HandoffIndica Raises: ProtocolError: If ``reserved`` does not fit its field. - Also if ``hi`` does not fit in one octet. + Also if ``hi`` is a name, a :obj:`bool` or a code its field + cannot carry. """ if option is not None: hi_val = option.hi # type: Enum_HandoffType reserved = option.reserved + self._make_uint(hi_val, bits=8, name='handoff type') else: - hi_val = self._make_index(hi, hi_default, namespace=hi_namespace, # type: ignore[assignment] - reversed=hi_reversed, pack=False) + hi_val = self._make_index(hi, Enum_HandoffType, bits=8, name='handoff type') - self._make_uint(hi_val, bits=8, name='handoff type') return Schema_HandoffIndicatorOption( type=type, length=2, @@ -9151,10 +9062,7 @@ def _make_opt_hi(self, type: 'Enum_Option', option: 'Optional[Data_HandoffIndica ) def _make_opt_att(self, type: 'Enum_Option', option: 'Optional[Data_AccessTechnologyTypeOption]' = None, *, - att: 'Enum_AccessType | StdlibEnum | AenumEnum | str | int' = Enum_AccessType.Virtual, - att_default: 'Optional[int]' = None, - att_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - att_reversed: 'bool' = False, + att: 'Enum_AccessType | int' = Enum_AccessType.Virtual, reserved: 'bytes' = b'\x00', **kwargs: 'Any') -> 'Schema_AccessTechnologyTypeOption': """Make MH access technology type option. @@ -9163,9 +9071,6 @@ def _make_opt_att(self, type: 'Enum_Option', option: 'Optional[Data_AccessTechno type: Option type. option: Option data model. att: Access technology type. - att_default: Default access technology type. - att_namespace: Access technology type namespace. - att_reversed: Reverse access technology type namespace. reserved: The reserved octet as captured, truncated or zero-filled to 1 octet. **kwargs: Arbitrary keyword arguments. @@ -9174,17 +9079,17 @@ def _make_opt_att(self, type: 'Enum_Option', option: 'Optional[Data_AccessTechno Constructed option schema. Raises: - ProtocolError: If ``att`` does not fit in one octet. + ProtocolError: If ``att`` is a name, a :obj:`bool` or a code its + field cannot carry. """ if option is not None: reserved = getattr(option, 'reserved', b'\x00') att_val = option.att # type: Enum_AccessType + self._make_uint(att_val, bits=8, name='access technology type') else: - att_val = self._make_index(att, att_default, namespace=att_namespace, # type: ignore[assignment] - reversed=att_reversed, pack=False) + att_val = self._make_index(att, Enum_AccessType, bits=8, name='access technology type') - self._make_uint(att_val, bits=8, name='access technology type') return Schema_AccessTechnologyTypeOption( type=type, length=2, @@ -9369,10 +9274,7 @@ def _make_opt_ipv4_hoa(self, type: 'Enum_Option', option: 'Optional[Data_IPv4Hom def _make_opt_ipv4_ack(self, type: 'Enum_Option', option: 'Optional[Data_IPv4AddressAcknowledgementOption]' = None, *, - status: 'Enum_DSMIPv6HomeAddress | StdlibEnum | AenumEnum | str | int' = Enum_DSMIPv6HomeAddress.Success, - status_default: 'Optional[int]' = None, - status_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - status_reversed: 'bool' = False, + status: 'Enum_DSMIPv6HomeAddress | int' = Enum_DSMIPv6HomeAddress.Success, prefix_length: 'int' = 32, address: 'bytes | str | int | IPv4Address' = '0.0.0.0', # nosec: B104 # not a bind reserved: 'int' = 0, @@ -9383,9 +9285,6 @@ def _make_opt_ipv4_ack(self, type: 'Enum_Option', type: Option type. option: Option data model. status: Status. - status_default: Default status. - status_namespace: Status namespace. - status_reversed: Reverse status namespace. prefix_length: Allocated prefix length. address: Assigned IPv4 home address. reserved: Reserved bits, i.e. the two bits after the prefix length. @@ -9397,7 +9296,8 @@ def _make_opt_ipv4_ack(self, type: 'Enum_Option', Raises: ProtocolError: If ``prefix_length`` or ``reserved`` is negative, a :obj:`bool`, or does not fit its field. - Also if ``status`` does not fit in one octet. + Also if ``status`` is a name, a :obj:`bool` or a code its field + cannot carry. Note: The prefix length is checked against its 6-bit field alone, as @@ -9415,13 +9315,12 @@ def _make_opt_ipv4_ack(self, type: 'Enum_Option', prefix_length = option.prefix_length reserved = option.reserved address = option.address + self._make_uint(status_val, bits=8, name='status') else: - status_val = self._make_index(status, status_default, namespace=status_namespace, # type: ignore[assignment] - reversed=status_reversed, pack=False) + status_val = self._make_index(status, Enum_DSMIPv6HomeAddress, bits=8, name='status') prefix_length = self._make_uint(prefix_length, bits=6, name='prefix length') - self._make_uint(status_val, bits=8, name='status') return Schema_IPv4AddressAcknowledgementOption( type=type, length=6, @@ -9534,10 +9433,7 @@ def _make_opt_gre(self, type: 'Enum_Option', option: 'Optional[Data_GREKeyOption def _make_opt_bid(self, type: 'Enum_Option', option: 'Optional[Data_BindingIdentifierOption]' = None, *, bid: 'int' = 1, - status: 'Enum_StatusCode | StdlibEnum | AenumEnum | str | int' = Enum_StatusCode.Binding_Update_accepted_Proxy_Binding_Update_accepted, - status_default: 'Optional[int]' = None, - status_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - status_reversed: 'bool' = False, + status: 'Enum_StatusCode | int' = Enum_StatusCode.Binding_Update_accepted_Proxy_Binding_Update_accepted, # pylint: disable=line-too-long simultaneous: 'bool' = False, bid_pri: 'int' = 0, address: 'Optional[bytes | str | int | IPv4Address | IPv6Address]' = None, @@ -9549,9 +9445,6 @@ def _make_opt_bid(self, type: 'Enum_Option', option: 'Optional[Data_BindingIdent option: Option data model. bid: Binding identifier. status: Status, overriding the message status for this binding alone. - status_default: Default status. - status_namespace: Status namespace. - status_reversed: Reverse status namespace. simultaneous: Simultaneous home and foreign binding flag. bid_pri: Binding priority. address: Care-of address, or :obj:`None` to omit it. @@ -9562,7 +9455,8 @@ def _make_opt_bid(self, type: 'Enum_Option', option: 'Optional[Data_BindingIdent Raises: ProtocolError: If ``bid`` or ``bid_pri`` does not fit its field. - Also if ``status`` does not fit in one octet. + Also if ``status`` is a name, a :obj:`bool` or a code its field + cannot carry. Note: The option length is derived from the address family, since that is @@ -9576,9 +9470,9 @@ def _make_opt_bid(self, type: 'Enum_Option', option: 'Optional[Data_BindingIdent simultaneous = option.simultaneous bid_pri = option.bid_pri address = option.address + self._make_uint(status_val, bits=8, name='status') else: - status_val = self._make_index(status, status_default, namespace=status_namespace, # type: ignore[assignment] - reversed=status_reversed, pack=False) + status_val = self._make_index(status, Enum_StatusCode, bits=8, name='status') bid = self._make_uint(bid, bits=16, name='binding identifier') bid_pri = self._make_uint(bid_pri, bits=7, name='binding priority') @@ -9599,7 +9493,6 @@ def _make_opt_bid(self, type: 'Enum_Option', option: 'Optional[Data_BindingIdent length = 8 if addr.version == 4 else 20 address = addr - self._make_uint(status_val, bits=8, name='status') return Schema_BindingIdentifierOption( type=type, length=length, @@ -9662,10 +9555,7 @@ def _make_opt_ipv4_hoa_req(self, type: 'Enum_Option', ) def _make_opt_ipv4_hoa_rep(self, type: 'Enum_Option', option: 'Optional[Data_IPv4HomeAddressReplyOption]' = None, *, - status: 'Enum_HomeAddressReply | StdlibEnum | AenumEnum | str | int' = Enum_HomeAddressReply.Success, - status_default: 'Optional[int]' = None, - status_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - status_reversed: 'bool' = False, + status: 'Enum_HomeAddressReply | int' = Enum_HomeAddressReply.Success, prefix_length: 'int' = 32, address: 'bytes | str | int | IPv4Address' = '0.0.0.0', # nosec: B104 # not a bind reserved: 'int' = 0, @@ -9676,9 +9566,6 @@ def _make_opt_ipv4_hoa_rep(self, type: 'Enum_Option', option: 'Optional[Data_IPv type: Option type. option: Option data model. status: Status. - status_default: Default status. - status_namespace: Status namespace. - status_reversed: Reverse status namespace. prefix_length: Prefix length of the assigned home network. address: Assigned IPv4 home address. reserved: Reserved bits, i.e. the two bits after the prefix length. @@ -9690,7 +9577,8 @@ def _make_opt_ipv4_hoa_rep(self, type: 'Enum_Option', option: 'Optional[Data_IPv Raises: ProtocolError: If ``prefix_length`` or ``reserved`` is negative, a :obj:`bool`, or does not fit its field. - Also if ``status`` does not fit in one octet. + Also if ``status`` is a name, a :obj:`bool` or a code its field + cannot carry. Note: The prefix length is checked against its 6-bit field alone, as @@ -9708,13 +9596,12 @@ def _make_opt_ipv4_hoa_rep(self, type: 'Enum_Option', option: 'Optional[Data_IPv prefix_length = option.prefix_length reserved = option.reserved address = option.address + self._make_uint(status_val, bits=8, name='status') else: - status_val = self._make_index(status, status_default, namespace=status_namespace, # type: ignore[assignment] - reversed=status_reversed, pack=False) + status_val = self._make_index(status, Enum_HomeAddressReply, bits=8, name='status') prefix_length = self._make_uint(prefix_length, bits=6, name='prefix length') - self._make_uint(status_val, bits=8, name='status') return Schema_IPv4HomeAddressReplyOption( type=type, length=6, @@ -9755,10 +9642,7 @@ def _make_opt_ipv4_router(self, type: 'Enum_Option', ) def _make_opt_ipv4_dhcp(self, type: 'Enum_Option', option: 'Optional[Data_IPv4DHCPSupportModeOption]' = None, *, - mode: 'Enum_DHCPSupportMode | StdlibEnum | AenumEnum | str | int' = Enum_DHCPSupportMode.Unassigned_0x0, - mode_default: 'Optional[int]' = None, - mode_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - mode_reversed: 'bool' = False, + mode: 'Enum_DHCPSupportMode | int' = Enum_DHCPSupportMode.Unassigned_0x0, reserved: 'int' = 0, **kwargs: 'Any') -> 'Schema_IPv4DHCPSupportModeOption': """Make MH IPv4 DHCP support mode option. @@ -9767,9 +9651,6 @@ def _make_opt_ipv4_dhcp(self, type: 'Enum_Option', option: 'Optional[Data_IPv4DH type: Option type. option: Option data model. mode: DHCP support mode. - mode_default: Default DHCP support mode. - mode_namespace: DHCP support mode namespace. - mode_reversed: Reverse DHCP support mode namespace. reserved: Reserved bits, i.e. the 15 bits before the ``S`` flag. **kwargs: Arbitrary keyword arguments. @@ -9778,17 +9659,15 @@ def _make_opt_ipv4_dhcp(self, type: 'Enum_Option', option: 'Optional[Data_IPv4DH Raises: ProtocolError: If the mode does not fit the one-bit ``S`` flag, or - ``reserved`` does not fit its field. + ``reserved`` does not fit its field; or if ``mode`` is a name. """ if option is not None: - mode_val = int(option.mode) + mode_val = self._make_uint(int(option.mode), bits=1, name='DHCP support mode') reserved = option.reserved else: - mode_val = self._make_index(mode, mode_default, namespace=mode_namespace, - reversed=mode_reversed, pack=False) - - mode_val = self._make_uint(mode_val, bits=1, name='DHCP support mode') + mode_val = int(self._make_index(mode, Enum_DHCPSupportMode, bits=1, + name='DHCP support mode')) return Schema_IPv4DHCPSupportModeOption( type=type, @@ -9817,7 +9696,8 @@ def _make_opt_cr(self, type: 'Enum_Option', option: 'Optional[Data_ContextReques Raises: ProtocolError: If the option is too long for its one-octet - ``Option Length``. + ``Option Length``, or the mobility option type of a pair is a + name, a :obj:`bool` or a code its field cannot carry. """ if option is not None: @@ -9833,7 +9713,7 @@ def _make_opt_cr(self, type: 'Enum_Option', option: 'Optional[Data_ContextReques req_type, value = int(request.type), request.value else: raw_type, value = request - req_type = int(raw_type) + req_type = int(self._make_index(raw_type, Enum_Option, bits=8, name='request type')) if len(value) > 0xFF: raise ProtocolError(f'{self.alias}: [OptNo {type}] context request value of ' @@ -9851,10 +9731,7 @@ def _make_opt_cr(self, type: 'Enum_Option', option: 'Optional[Data_ContextReques ) def _make_opt_lmaa(self, type: 'Enum_Option', option: 'Optional[Data_LMAAddressOption]' = None, *, - code: 'Optional[LMAAddressCode | StdlibEnum | AenumEnum | str | int]' = None, - code_default: 'Optional[int]' = None, - code_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - code_reversed: 'bool' = False, + code: 'Optional[LMAAddressCode | int]' = None, address: 'bytes | str | int | IPv4Address | IPv6Address' = '::', reserved: 'bytes' = b'\x00', **kwargs: 'Any') -> 'Schema_LMAAddressOption': @@ -9866,9 +9743,6 @@ def _make_opt_lmaa(self, type: 'Enum_Option', option: 'Optional[Data_LMAAddressO code: Option code, see :class:`~pcapkit.protocols.internet.mh.LMAAddressCode`. If not given, the code of the address family of ``address``. - code_default: Default option code. - code_namespace: Option code namespace. - code_reversed: Reverse option code namespace. address: Local mobility anchor address. reserved: The reserved octet as captured, truncated or zero-filled to 1 octet. @@ -9880,7 +9754,8 @@ def _make_opt_lmaa(self, type: 'Enum_Option', option: 'Optional[Data_LMAAddressO Raises: ProtocolError: If an explicit ``code`` is not the code :rfc:`5949#section-6.2.2` gives the address family of - ``address`` (:issue:`2280`), or does not fit in one octet. + ``address`` (:issue:`2280`), or is a name, a :obj:`bool` or a + code its field cannot carry. Note: The option length is derived from the address family rather than from @@ -9891,14 +9766,12 @@ def _make_opt_lmaa(self, type: 'Enum_Option', option: 'Optional[Data_LMAAddressO """ if option is not None: reserved = getattr(option, 'reserved', b'\x00') - code_val = int(option.code) # type: Optional[int] + code_val = self._make_uint(int(option.code), bits=8, name='code') # type: Optional[int] address = option.address elif code is None: code_val = None else: - code_val = self._make_index(code, code_default, - namespace=code_namespace if code_namespace is not None else LMAAddressCode, - reversed=code_reversed, pack=False) + code_val = self._make_index(code, LMAAddressCode, bits=8, name='code') if isinstance(address, bytes) and len(address) not in (4, 16): raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid address: {address!r}') @@ -9928,7 +9801,7 @@ def _make_opt_lmaa(self, type: 'Enum_Option', option: 'Optional[Data_LMAAddressO return Schema_LMAAddressOption( type=type, length=6 if addr.version == 4 else 18, - code=self._make_uint(code_val, bits=8, name='code'), + code=code_val, reserved=reserved, address=addr, ) @@ -10042,7 +9915,7 @@ def _make_opt_fs(self, type: 'Enum_Option', option: 'Optional[Data_FlowSummaryOp def _make_fid_suboptions( self, - suboptions: 'FlowIDSuboption | list[Schema_FlowIdentificationSuboption | tuple[Enum_FlowIDSuboption, dict[str, Any]] | bytes]', + suboptions: 'FlowIDSuboption | list[Schema_FlowIdentificationSuboption | tuple[Enum_FlowIDSuboption | int, dict[str, Any]] | bytes]', # pylint: disable=line-too-long ) -> 'list[Schema_FlowIdentificationSuboption | bytes]': """Make MH flow identification sub-options. @@ -10053,6 +9926,11 @@ def _make_fid_suboptions( Returns: Sub-option schema list. + Raises: + ProtocolError: If the type of a sub-option, given in a ``(type, + kwargs)`` pair or as the key of parsed data, is a name, a + :obj:`bool` or a code its field cannot carry. + """ if isinstance(suboptions, list): entries = [] # type: list[Schema_FlowIdentificationSuboption | bytes] @@ -10060,12 +9938,14 @@ def _make_fid_suboptions( if isinstance(item, (bytes, Schema)): entries.append(cast('Schema_FlowIdentificationSuboption | bytes', item)) else: - code, args = cast('tuple[Enum_FlowIDSuboption, dict[str, Any]]', item) + kind, args = cast('tuple[Enum_FlowIDSuboption | int, dict[str, Any]]', item) + code = self._make_index(kind, Enum_FlowIDSuboption, bits=8, name='sub-option type') entries.append(self._make_fid_suboption(code, **args)) return entries - return [self._make_fid_suboption(code, option) - for code, option in suboptions.items(multi=True)] + return [self._make_fid_suboption( + self._make_index(key, Enum_FlowIDSuboption, bits=8, name='sub-option type'), option) + for key, option in suboptions.items(multi=True)] def _make_fid_suboption(self, code: 'Enum_FlowIDSuboption', option: 'Optional[Data_FlowIdentificationSuboption]' = None, @@ -10082,7 +9962,8 @@ def _make_fid_suboption(self, code: 'Enum_FlowIDSuboption', Raises: ProtocolError: If an entry of a ``BID_Reference`` sub-option's ``bid`` does - not fit its field. + not fit its field, or a ``ts_format`` or ``action`` is a name, a + :obj:`bool` or a code its field cannot carry. Note: The data model parameter is named ``option`` rather than ``data`` @@ -10128,9 +10009,9 @@ def _make_fid_suboption(self, code: 'Enum_FlowIDSuboption', selector = selector_opt.selector reserved = getattr(selector_opt, 'reserved', b'\x00') else: - ts_format = cast('Enum_TrafficSelector | int', - kwargs.get('ts_format', - Enum_TrafficSelector.IPv6_Binary_Traffic_Selector)) + ts_format = self._make_index( + kwargs.get('ts_format', Enum_TrafficSelector.IPv6_Binary_Traffic_Selector), + Enum_TrafficSelector, bits=8, name='TS format') selector = cast('bytes', kwargs.get('selector', b'')) reserved = cast('bytes', kwargs.get('reserved', b'\x00')) return Schema_TrafficSelectorSuboption( @@ -10144,8 +10025,8 @@ def _make_fid_suboption(self, code: 'Enum_FlowIDSuboption', option).action # type: Enum_FlowBindingAction | int reserved = getattr(option, 'reserved', b'\x00') else: - action = cast('Enum_FlowBindingAction | int', - kwargs.get('action', Enum_FlowBindingAction.Add)) + action = self._make_index(kwargs.get('action', Enum_FlowBindingAction.Add), + Enum_FlowBindingAction, bits=8, name='action') reserved = cast('bytes', kwargs.get('reserved', b'\x00')) return Schema_FlowBindingActionSuboption( type=code, length=2, reserved=reserved, @@ -10178,11 +10059,8 @@ def _make_fid_suboption(self, code: 'Enum_FlowIDSuboption', def _make_opt_fid(self, type: 'Enum_Option', option: 'Optional[Data_FlowIdentificationOption]' = None, *, fid: 'int' = 1, fid_pri: 'int' = 1, - status: 'Enum_FlowIDStatus | StdlibEnum | AenumEnum | str | int' = Enum_FlowIDStatus.Flow_binding_successful, - status_default: 'Optional[int]' = None, - status_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - status_reversed: 'bool' = False, - suboptions: 'Optional[FlowIDSuboption | list[Schema_FlowIdentificationSuboption | tuple[Enum_FlowIDSuboption, dict[str, Any]] | bytes]]' = None, + status: 'Enum_FlowIDStatus | int' = Enum_FlowIDStatus.Flow_binding_successful, + suboptions: 'Optional[FlowIDSuboption | list[Schema_FlowIdentificationSuboption | tuple[Enum_FlowIDSuboption | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long reserved: 'bytes' = b'\x00', **kwargs: 'Any') -> 'Schema_FlowIdentificationOption': """Make MH flow identification mobility option. @@ -10193,9 +10071,6 @@ def _make_opt_fid(self, type: 'Enum_Option', option: 'Optional[Data_FlowIdentifi fid: Flow identifier. fid_pri: Flow priority. status: Status. - status_default: Default status. - status_namespace: Status namespace. - status_reversed: Reverse status namespace. suboptions: Sub-options. reserved: The reserved octet as captured, truncated or zero-filled to 1 octet. @@ -10206,8 +10081,9 @@ def _make_opt_fid(self, type: 'Enum_Option', option: 'Optional[Data_FlowIdentifi Raises: ProtocolError: If ``fid`` or ``fid_pri`` does not fit its field. - Also if ``status`` does not fit in one octet, or the option is too long - for its one-octet ``Option Length``. + Also if ``status`` is a name, a :obj:`bool` or a code its field + cannot carry, or the option is too long for its one-octet + ``Option Length``. """ if option is not None: @@ -10216,16 +10092,15 @@ def _make_opt_fid(self, type: 'Enum_Option', option: 'Optional[Data_FlowIdentifi fid_pri = option.fid_pri status_val = option.status # type: Enum_FlowIDStatus suboptions = option.suboptions + self._make_uint(status_val, bits=8, name='status') else: - status_val = self._make_index(status, status_default, namespace=status_namespace, # type: ignore[assignment] - reversed=status_reversed, pack=False) + status_val = self._make_index(status, Enum_FlowIDStatus, bits=8, name='status') suboptions = suboptions or [] entries = self._make_fid_suboptions(suboptions) length = 6 + sum(len(entry) if isinstance(entry, bytes) else len(entry.pack()) for entry in entries) - self._make_uint(status_val, bits=8, name='status') return Schema_FlowIdentificationOption( type=type, length=self._make_uint(length, bits=8, name='Option Length'), @@ -10376,10 +10251,7 @@ def _make_opt_alt_ipv4_coa(self, type: 'Enum_Option', ) def _make_opt_mn_group(self, type: 'Enum_Option', option: 'Optional[Data_MNGroupIdentifierOption]' = None, *, - subtype: 'Enum_MNGroupID | StdlibEnum | AenumEnum | str | int' = Enum_MNGroupID.Bulk_Binding_Update_Group, - subtype_default: 'Optional[int]' = None, - subtype_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - subtype_reversed: 'bool' = False, + subtype: 'Enum_MNGroupID | int' = Enum_MNGroupID.Bulk_Binding_Update_Group, group_id: 'int' = 1, reserved: 'bytes' = b'\x00', **kwargs: 'Any') -> 'Schema_MNGroupIdentifierOption': @@ -10389,9 +10261,6 @@ def _make_opt_mn_group(self, type: 'Enum_Option', option: 'Optional[Data_MNGroup type: Option type. option: Option data model. subtype: Sub-type. - subtype_default: Default sub-type. - subtype_namespace: Sub-type namespace. - subtype_reversed: Reverse sub-type namespace. group_id: Mobile node group identifier. reserved: The reserved octet as captured, truncated or zero-filled to 1 octet. @@ -10402,18 +10271,19 @@ def _make_opt_mn_group(self, type: 'Enum_Option', option: 'Optional[Data_MNGroup Raises: ProtocolError: If ``group_id`` does not fit its field. - Also if ``subtype`` does not fit in one octet. + Also if ``subtype`` is a name, a :obj:`bool` or a code its field + cannot carry. """ if option is not None: reserved = getattr(option, 'reserved', b'\x00') subtype_val = option.subtype # type: Enum_MNGroupID group_id = option.group_id + self._make_uint(subtype_val, bits=8, name='group identifier subtype') else: - subtype_val = self._make_index(subtype, subtype_default, namespace=subtype_namespace, # type: ignore[assignment] - reversed=subtype_reversed, pack=False) + subtype_val = self._make_index(subtype, Enum_MNGroupID, bits=8, + name='group identifier subtype') - self._make_uint(subtype_val, bits=8, name='group identifier subtype') return Schema_MNGroupIdentifierOption( type=type, length=6, @@ -10461,7 +10331,7 @@ def _make_opt_mag_addr(self, type: 'Enum_Option', option: 'Optional[Data_MAGIPv6 def _make_ani_suboptions( self, - suboptions: 'ANISuboption | list[Schema_ANISuboption | tuple[Enum_ANISuboption, dict[str, Any]] | bytes]', + suboptions: 'ANISuboption | list[Schema_ANISuboption | tuple[Enum_ANISuboption | int, dict[str, Any]] | bytes]', ) -> 'list[Schema_ANISuboption | bytes]': """Make MH access network identifier sub-options. @@ -10472,6 +10342,11 @@ def _make_ani_suboptions( Returns: Sub-option schema list. + Raises: + ProtocolError: If the type of a sub-option, given in a ``(type, + kwargs)`` pair or as the key of parsed data, is a name, a + :obj:`bool` or a code its field cannot carry. + """ if isinstance(suboptions, list): entries = [] # type: list[Schema_ANISuboption | bytes] @@ -10479,12 +10354,14 @@ def _make_ani_suboptions( if isinstance(item, (bytes, Schema)): entries.append(cast('Schema_ANISuboption | bytes', item)) else: - code, args = cast('tuple[Enum_ANISuboption, dict[str, Any]]', item) + kind, args = cast('tuple[Enum_ANISuboption | int, dict[str, Any]]', item) + code = self._make_index(kind, Enum_ANISuboption, bits=8, name='sub-option type') entries.append(self._make_ani_suboption(code, **args)) return entries - return [self._make_ani_suboption(code, option) - for code, option in suboptions.items(multi=True)] + return [self._make_ani_suboption( + self._make_index(key, Enum_ANISuboption, bits=8, name='sub-option type'), option) + for key, option in suboptions.items(multi=True)] def _make_ani_suboption(self, code: 'Enum_ANISuboption', option: 'Optional[Data_ANISuboption]' = None, @@ -10502,7 +10379,9 @@ def _make_ani_suboption(self, code: 'Enum_ANISuboption', Raises: ProtocolError: If a raw geo-location degree does not fit in 24 bits as two's complement, or a sub-option's ``reserved``, - ``format``, ``group_id`` or ``timer`` does not fit its field. + ``format``, ``group_id`` or ``timer`` does not fit its field, or + ``op_id_type`` is a name, a :obj:`bool` or a code its field + cannot carry. Note: The geo-location degrees are re-encoded from the **raw** signed @@ -10553,8 +10432,9 @@ def _make_ani_suboption(self, code: 'Enum_ANISuboption', op_id_type = operator.op_id_type # type: Enum_OperatorID | int identifier = operator.identifier else: - op_id_type = cast('Enum_OperatorID | int', - kwargs.get('op_id_type', Enum_OperatorID.Realm_of_the_Operator)) + op_id_type = self._make_index( + kwargs.get('op_id_type', Enum_OperatorID.Realm_of_the_Operator), + Enum_OperatorID, bits=8, name='Op-ID type') identifier = cast('bytes', kwargs.get('identifier', b'')) return Schema_ANIOperatorIdentifierSuboption( type=code, length=1 + len(identifier), @@ -10601,7 +10481,7 @@ def _make_ani_suboption(self, code: 'Enum_ANISuboption', return Schema_UnassignedANISuboption(type=code, length=len(payload), data=payload) def _make_opt_ani(self, type: 'Enum_Option', option: 'Optional[Data_AccessNetworkIdentifierOption]' = None, *, - suboptions: 'Optional[ANISuboption | list[Schema_ANISuboption | tuple[Enum_ANISuboption, dict[str, Any]] | bytes]]' = None, + suboptions: 'Optional[ANISuboption | list[Schema_ANISuboption | tuple[Enum_ANISuboption | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long **kwargs: 'Any') -> 'Schema_AccessNetworkIdentifierOption': """Make MH access network identifier option. @@ -10640,7 +10520,7 @@ def _make_opt_ani(self, type: 'Enum_Option', option: 'Optional[Data_AccessNetwor def _make_opt_offload(self, type: 'Enum_Option', option: 'Optional[Data_IPv4TrafficOffloadSelectorOption]' = None, *, mode: 'bool' = False, - selector: 'Optional[FlowIDSuboption | list[Schema_FlowIdentificationSuboption | tuple[Enum_FlowIDSuboption, dict[str, Any]] | bytes]]' = None, + selector: 'Optional[FlowIDSuboption | list[Schema_FlowIdentificationSuboption | tuple[Enum_FlowIDSuboption | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long reserved: 'int' = 0, **kwargs: 'Any') -> 'Schema_IPv4TrafficOffloadSelectorOption': """Make MH IPv4 traffic offload selector option. @@ -10855,7 +10735,7 @@ def _make_opt_ams6(self, type: 'Enum_Option', def _make_qos_attributes( self, - attributes: 'QoSAttribute | list[Schema_QoSAttribute | tuple[Enum_QoSAttribute, dict[str, Any]] | bytes]', + attributes: 'QoSAttribute | list[Schema_QoSAttribute | tuple[Enum_QoSAttribute | int, dict[str, Any]] | bytes]', ) -> 'list[Schema_QoSAttribute | bytes]': """Make MH quality-of-service attributes. @@ -10866,6 +10746,11 @@ def _make_qos_attributes( Returns: Attribute schema list. + Raises: + ProtocolError: If the type of an attribute, given in a ``(type, + kwargs)`` pair or as the key of parsed data, is a name, a + :obj:`bool` or a code its field cannot carry. + """ if isinstance(attributes, list): entries = [] # type: list[Schema_QoSAttribute | bytes] @@ -10873,12 +10758,14 @@ def _make_qos_attributes( if isinstance(item, (bytes, Schema)): entries.append(cast('Schema_QoSAttribute | bytes', item)) else: - code, args = cast('tuple[Enum_QoSAttribute, dict[str, Any]]', item) + kind, args = cast('tuple[Enum_QoSAttribute | int, dict[str, Any]]', item) + code = self._make_index(kind, Enum_QoSAttribute, bits=8, name='attribute type') entries.append(self._make_qos_attribute(code, **args)) return entries - return [self._make_qos_attribute(code, option) - for code, option in attributes.items(multi=True)] + return [self._make_qos_attribute( + self._make_index(key, Enum_QoSAttribute, bits=8, name='attribute type'), option) + for key, option in attributes.items(multi=True)] def _make_qos_attribute(self, code: 'Enum_QoSAttribute', option: 'Optional[Data_QoSAttribute]' = None, @@ -10895,7 +10782,8 @@ def _make_qos_attribute(self, code: 'Enum_QoSAttribute', Raises: ProtocolError: If a rate, priority, vendor or reserved value does not fit - its field. + its field, or ``ts_format`` is a name, a :obj:`bool` or a code its + field cannot carry. Note: The data model parameter is named ``option`` rather than ``data`` @@ -10970,9 +10858,9 @@ def _make_qos_attribute(self, code: 'Enum_QoSAttribute', selector = ts_attr.selector octets = getattr(ts_attr, 'reserved', b'\x00') else: - ts_format = cast('Enum_TrafficSelector | int', - kwargs.get('ts_format', - Enum_TrafficSelector.IPv6_Binary_Traffic_Selector)) + ts_format = self._make_index( + kwargs.get('ts_format', Enum_TrafficSelector.IPv6_Binary_Traffic_Selector), + Enum_TrafficSelector, bits=8, name='TS format') selector = cast('bytes', kwargs.get('selector', b'')) octets = cast('bytes', kwargs.get('reserved', b'\x00')) return Schema_QoSTrafficSelectorAttribute( @@ -11005,7 +10893,7 @@ def _make_opt_qos(self, type: 'Enum_Option', option: 'Optional[Data_QualityOfSer sr_id: 'int' = 0, dscp: 'int' = 0, oc: 'int' = 0, - attributes: 'Optional[QoSAttribute | list[Schema_QoSAttribute | tuple[Enum_QoSAttribute, dict[str, Any]] | bytes]]' = None, + attributes: 'Optional[QoSAttribute | list[Schema_QoSAttribute | tuple[Enum_QoSAttribute | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long reserved: 'bytes' = b'\x00\x00\x00', tc_reserved: 'int' = 0, **kwargs: 'Any') -> 'Schema_QualityOfServiceOption': @@ -11195,7 +11083,7 @@ def _make_opt_mcast_ack(self, type: 'Enum_Option', def _make_lcmp_suboptions( self, - suboptions: 'LMAControlledMAGSuboption | list[Schema_LMAControlledMAGSuboption | tuple[Enum_LMAControlledMAGSuboption, dict[str, Any]] | bytes]', + suboptions: 'LMAControlledMAGSuboption | list[Schema_LMAControlledMAGSuboption | tuple[Enum_LMAControlledMAGSuboption | int, dict[str, Any]] | bytes]', # pylint: disable=line-too-long ) -> 'list[Schema_LMAControlledMAGSuboption | bytes]': """Make MH LMA-controlled MAG parameters sub-options. @@ -11206,6 +11094,11 @@ def _make_lcmp_suboptions( Returns: Sub-option schema list. + Raises: + ProtocolError: If the type of a sub-option, given in a ``(type, + kwargs)`` pair or as the key of parsed data, is a name, a + :obj:`bool` or a code its field cannot carry. + """ if isinstance(suboptions, list): entries = [] # type: list[Schema_LMAControlledMAGSuboption | bytes] @@ -11213,13 +11106,17 @@ def _make_lcmp_suboptions( if isinstance(item, (bytes, Schema)): entries.append(cast('Schema_LMAControlledMAGSuboption | bytes', item)) else: - code, args = cast( - 'tuple[Enum_LMAControlledMAGSuboption, dict[str, Any]]', item) + kind, args = cast( + 'tuple[Enum_LMAControlledMAGSuboption | int, dict[str, Any]]', item) + code = self._make_index(kind, Enum_LMAControlledMAGSuboption, bits=8, + name='sub-option type') entries.append(self._make_lcmp_suboption(code, **args)) return entries - return [self._make_lcmp_suboption(code, option) - for code, option in suboptions.items(multi=True)] + return [self._make_lcmp_suboption( + self._make_index(key, Enum_LMAControlledMAGSuboption, bits=8, + name='sub-option type'), option) + for key, option in suboptions.items(multi=True)] @staticmethod def _seconds(value: 'int | timedelta', unit: 'int' = 1) -> 'int': @@ -11304,7 +11201,7 @@ def _make_lcmp_suboption(self, code: 'Enum_LMAControlledMAGSuboption', type=code, length=len(payload), data=payload) def _make_opt_lcmp(self, type: 'Enum_Option', option: 'Optional[Data_LMAControlledMAGParametersOption]' = None, *, - suboptions: 'Optional[LMAControlledMAGSuboption | list[Schema_LMAControlledMAGSuboption | tuple[Enum_LMAControlledMAGSuboption, dict[str, Any]] | bytes]]' = None, + suboptions: 'Optional[LMAControlledMAGSuboption | list[Schema_LMAControlledMAGSuboption | tuple[Enum_LMAControlledMAGSuboption | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long **kwargs: 'Any') -> 'Schema_LMAControlledMAGParametersOption': """Make MH LMA-controlled MAG parameters option. @@ -11341,10 +11238,7 @@ def _make_opt_lcmp(self, type: 'Enum_Option', option: 'Optional[Data_LMAControll ) def _make_opt_mag_mp(self, type: 'Enum_Option', option: 'Optional[Data_MAGMultipathBindingOption]' = None, *, - att: 'Enum_AccessType | StdlibEnum | AenumEnum | str | int' = Enum_AccessType.Virtual, - att_default: 'Optional[int]' = None, - att_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - att_reversed: 'bool' = False, + att: 'Enum_AccessType | int' = Enum_AccessType.Virtual, label: 'int' = 0, bid: 'int' = 1, bulk: 'bool' = False, @@ -11357,9 +11251,6 @@ def _make_opt_mag_mp(self, type: 'Enum_Option', option: 'Optional[Data_MAGMultip type: Option type. option: Option data model. att: Interface access-technology type. - att_default: Default access-technology type. - att_namespace: Access-technology type namespace. - att_reversed: Reverse access-technology type namespace. label: Interface label. bid: Binding identifier; ``0`` and ``255`` are reserved. bulk: Bulk re-registration flag. @@ -11374,7 +11265,8 @@ def _make_opt_mag_mp(self, type: 'Enum_Option', option: 'Optional[Data_MAGMultip ProtocolError: If both flags are set, which :rfc:`8278#section-4.1` forbids, or if ``label``, ``bid`` or ``reserved`` does not fit its field. - Also if ``att`` does not fit in one octet. + Also if ``att`` is a name, a :obj:`bool` or a code its field + cannot carry. """ if option is not None: @@ -11384,15 +11276,14 @@ def _make_opt_mag_mp(self, type: 'Enum_Option', option: 'Optional[Data_MAGMultip bulk = option.bulk overwrite = option.overwrite reserved = option.reserved + self._make_uint(att_val, bits=8, name='access technology type') else: - att_val = self._make_index(att, att_default, namespace=att_namespace, # type: ignore[assignment] - reversed=att_reversed, pack=False) + att_val = self._make_index(att, Enum_AccessType, bits=8, name='access technology type') if bulk and overwrite: raise ProtocolError(f'{self.alias}: [OptNo {type}] the bulk re-registration and ' 'registration overwrite flags are mutually exclusive') - self._make_uint(att_val, bits=8, name='access technology type') return Schema_MAGMultipathBindingOption( type=type, length=6, @@ -11407,10 +11298,7 @@ def _make_opt_mag_mp(self, type: 'Enum_Option', option: 'Optional[Data_MAGMultip ) def _make_opt_mag_id(self, type: 'Enum_Option', option: 'Optional[Data_MAGIdentifierOption]' = None, *, - subtype: 'Enum_MNIDSubtype | StdlibEnum | AenumEnum | str | int' = Enum_MNIDSubtype.NAI, - subtype_default: 'Optional[int]' = None, - subtype_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - subtype_reversed: 'bool' = False, + subtype: 'Enum_MNIDSubtype | int' = Enum_MNIDSubtype.NAI, identifier: 'bytes' = b'', reserved: 'bytes' = b'\x00', **kwargs: 'Any') -> 'Schema_MAGIdentifierOption': @@ -11420,9 +11308,6 @@ def _make_opt_mag_id(self, type: 'Enum_Option', option: 'Optional[Data_MAGIdenti type: Option type. option: Option data model. subtype: Sub-type, from the mobile node identifier subtype registry. - subtype_default: Default sub-type. - subtype_namespace: Sub-type namespace. - subtype_reversed: Reverse sub-type namespace. identifier: Identifier. reserved: The reserved octet as captured, truncated or zero-filled to 1 octet. @@ -11432,19 +11317,20 @@ def _make_opt_mag_id(self, type: 'Enum_Option', option: 'Optional[Data_MAGIdenti Constructed option schema. Raises: - ProtocolError: If ``subtype`` does not fit in one octet, or the option is - too long for its one-octet ``Option Length``. + ProtocolError: If ``subtype`` is a name, a :obj:`bool` or a code its + field cannot carry, or the option is too long for its one-octet + ``Option Length``. """ if option is not None: reserved = getattr(option, 'reserved', b'\x00') subtype_val = option.subtype # type: Enum_MNIDSubtype identifier = option.identifier + self._make_uint(subtype_val, bits=8, name='MAG identifier subtype') else: - subtype_val = self._make_index(subtype, subtype_default, namespace=subtype_namespace, # type: ignore[assignment] - reversed=subtype_reversed, pack=False) + subtype_val = self._make_index(subtype, Enum_MNIDSubtype, bits=8, + name='MAG identifier subtype') - self._make_uint(subtype_val, bits=8, name='MAG identifier subtype') return Schema_MAGIdentifierOption( type=type, length=self._make_uint(2 + len(identifier), bits=8, name='Option Length'), @@ -11661,7 +11547,7 @@ def _make_opt_dlif_lladdr(self, type: 'Enum_Option', option: 'Optional[Data_DLIF lla=lla, ) - def _make_cga_extensions(self, extensions: 'Extension | list[Schema_CGAExtension | tuple[Enum_CGAExtension, dict[str, Any]] | bytes]') -> 'tuple[list[Schema_CGAExtension | bytes], int]': + def _make_cga_extensions(self, extensions: 'Extension | list[Schema_CGAExtension | tuple[Enum_CGAExtension | int, dict[str, Any]] | bytes]') -> 'tuple[list[Schema_CGAExtension | bytes], int]': # pylint: disable=line-too-long """Make CGA extensions for MH. Args: @@ -11670,6 +11556,11 @@ def _make_cga_extensions(self, extensions: 'Extension | list[Schema_CGAExtension Returns: Tuple of extensions and total length of extensions. + Raises: + ProtocolError: If the type of an extension, given in a ``(type, + kwargs)`` pair or as the key of parsed data, is a name, a + :obj:`bool` or a code its field cannot carry. + """ total_length = 0 if isinstance(extensions, list): @@ -11684,7 +11575,9 @@ def _make_cga_extensions(self, extensions: 'Extension | list[Schema_CGAExtension data = schema data_len = len(schema.pack()) else: - code, args = cast('tuple[Enum_CGAExtension, dict[str, Any]]', schema) + kind, args = cast('tuple[Enum_CGAExtension | int, dict[str, Any]]', schema) + code = self._make_index(kind, Enum_CGAExtension, bits=16, + name='extension type') name = self._lookup_registry(self.__extension__, code) if isinstance(name, str): meth_name = f'_make_ext_{name}' @@ -11701,7 +11594,8 @@ def _make_cga_extensions(self, extensions: 'Extension | list[Schema_CGAExtension return extensions_list, total_length extensions_list = [] - for code, extension in extensions.items(multi=True): + for key, extension in extensions.items(multi=True): + code = self._make_index(key, Enum_CGAExtension, bits=16, name='extension type') name = self._lookup_registry(self.__extension__, code) if isinstance(name, str): meth_name = f'_make_ext_{name}' diff --git a/pcapkit/protocols/link/arp.py b/pcapkit/protocols/link/arp.py index 6d62d09994..ae6f175367 100644 --- a/pcapkit/protocols/link/arp.py +++ b/pcapkit/protocols/link/arp.py @@ -59,11 +59,9 @@ from pcapkit.utilities.exceptions import FieldValueError, ProtocolError if TYPE_CHECKING: - from enum import IntEnum as StdlibEnum from ipaddress import IPv4Address, IPv6Address - from typing import Any, Optional, Type + from typing import Any, Optional - from aenum import IntEnum as AenumEnum from typing_extensions import Literal from pcapkit.protocols.protocol import ProtocolBase @@ -90,6 +88,23 @@ class ARP(Link[Data_ARP, Schema_ARP], :meth:`read` takes an IPv4 or IPv6 protocol address only when both ``ptype`` and the address length agree (:issue:`1206`). + .. versionchanged:: 1.6.0 + + ``htype``, ``ptype`` and ``oper`` of :meth:`make` each take a member or + an :class:`int` code, resolved with the enumeration's ``get``; a name, a + :obj:`bool`, or a code the field cannot carry raises + :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than + resolving against an enumeration inferred from the caller's frame or + writing a different value (:issue:`2594`). + + .. versionremoved:: 1.6.0 + + The ``_default``, ``_namespace`` and ``_reversed`` arguments that went + with each of ``htype``, ``ptype`` and ``oper`` of :meth:`make`, such as + ``htype_default``; resolve a name with + :meth:`~pcapkit.protocols.protocol.Protocol.resolve_index` first + (:issue:`2594`). + """ #: Name of corresponding protocol. _name: 'Literal["Address Resolution Protocol", "Inverse Address Resolution Protocol", "Reverse Address Resolution Protocol", "Dynamic Reverse Address Resolution Protocol"]' # pylint: disable=line-too-long @@ -212,20 +227,11 @@ def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_ARP': return self._decode_next_layer(arp, -1, length - arp.len) def make(self, *, - htype: 'Enum_Hardware | StdlibEnum | AenumEnum | str | int' = Enum_Hardware.Ethernet, - htype_default: 'Optional[int]' = None, - htype_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - htype_reversed: 'bool' = False, - ptype: 'Enum_EtherType | StdlibEnum | AenumEnum | str | int' = Enum_EtherType.Internet_Protocol_version_4, - ptype_default: 'Optional[int]' = None, - ptype_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - ptype_reversed: 'bool' = False, + htype: 'Enum_Hardware | int' = Enum_Hardware.Ethernet, + ptype: 'Enum_EtherType | int' = Enum_EtherType.Internet_Protocol_version_4, hlen: 'Optional[int]' = None, plen: 'Optional[int]' = None, - oper: 'Enum_Operation | StdlibEnum | AenumEnum | str | int' = Enum_Operation.REQUEST, - oper_default: 'Optional[int]' = None, - oper_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - oper_reversed: 'bool' = False, + oper: 'Enum_Operation | int' = Enum_Operation.REQUEST, sha: 'str | bytes | bytearray' = '00:00:00:00:00:00', spa: 'IPv4Address | IPv6Address | str | bytes | bytearray' = '0.0.0.0', # nosec: B104 tha: 'str | bytes | bytearray' = '00:00:00:00:00:00', @@ -236,21 +242,12 @@ def make(self, *, Args: htype: Hardware type. - htype_default: Default value of hardware type. - htype_namespace: Namespace of hardware type. - htype_reversed: Reversed flag of hardware type. ptype: Protocol type. - ptype_default: Default value of protocol type. - ptype_namespace: Namespace of protocol type. - ptype_reversed: Reversed flag of protocol type. hlen: Hardware address length; if not given, ``6`` when ``htype`` is Ethernet, otherwise derived from the packed ``sha``. plen: Protocol address length; derived from the packed ``spa`` if not given. oper: Operation. - oper_default: Default value of operation. - oper_namespace: Namespace of operation. - oper_reversed: Reversed flag of operation. sha: Sender hardware address. spa: Sender protocol address. tha: Target hardware address. @@ -264,7 +261,9 @@ def make(self, *, Raises: FieldValueError: If ``spa`` or ``tpa`` is rejected by :meth:`_make_proto_resolve`, e.g. a :obj:`bool`. - ProtocolError: If ``sha`` or ``tha`` is rejected by + ProtocolError: If ``htype``, ``ptype`` or ``oper`` is a name, a + :obj:`bool`, or a member or code its field cannot carry; if + ``sha`` or ``tha`` is rejected by :meth:`_make_addr_resolve`, or ``spa`` or ``tpa`` by :meth:`_make_proto_resolve`; if a packed hardware address is not ``hlen`` octets long (with ``hlen`` not given and @@ -272,12 +271,14 @@ def make(self, *, address is not ``plen`` octets long. """ - _htype = self._make_index(htype, htype_default, namespace=htype_namespace, - reversed=htype_reversed, pack=False) - _ptype = self._make_index(ptype, ptype_default, namespace=ptype_namespace, - reversed=ptype_reversed, pack=False) - _oper = self._make_index(oper, oper_default, namespace=oper_namespace, - reversed=oper_reversed, pack=False) + # NOTE: prefixed with the class name, for the reason + # ``_make_proto_resolve`` gives: ``alias`` is unset before ``read()``. + _htype = self._make_index(htype, Enum_Hardware, bits=16, name='hardware type', + alias=self.__class__.__name__) + _ptype = self._make_index(ptype, Enum_EtherType, bits=16, name='protocol type', + alias=self.__class__.__name__) + _oper = self._make_index(oper, Enum_Operation, bits=16, name='operation', + alias=self.__class__.__name__) _sha = self._make_addr_resolve(sha, _htype) _spa = self._make_proto_resolve(spa, _ptype, plen) diff --git a/pcapkit/protocols/link/ethernet.py b/pcapkit/protocols/link/ethernet.py index d828ee7ae1..dd700ce7dc 100644 --- a/pcapkit/protocols/link/ethernet.py +++ b/pcapkit/protocols/link/ethernet.py @@ -34,10 +34,8 @@ from pcapkit.protocols.schema.link.ethernet import Ethernet as Schema_Ethernet if TYPE_CHECKING: - from enum import IntEnum as StdlibEnum - from typing import Any, Optional, Type + from typing import Any, Optional - from aenum import IntEnum as AenumEnum from typing_extensions import Literal from pcapkit.protocols.protocol import ProtocolBase @@ -48,7 +46,25 @@ class Ethernet(Link[Data_Ethernet, Schema_Ethernet], schema=Schema_Ethernet, data=Data_Ethernet): - """This class implements Ethernet Protocol.""" + """This class implements Ethernet Protocol. + + .. versionchanged:: 1.6.0 + + ``type`` of :meth:`make` takes a member or an :class:`int` code, + resolved with the enumeration's ``get``; a name, a :obj:`bool`, or a + code the field cannot carry raises + :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than + resolving against an enumeration inferred from the caller's frame or + writing a different value (:issue:`2594`). + + .. versionremoved:: 1.6.0 + + The ``type_default``, ``type_namespace`` and ``type_reversed`` + arguments of :meth:`make`; resolve a name with + :meth:`~pcapkit.protocols.protocol.Protocol.resolve_index` first + (:issue:`2594`). + + """ ########################################################################## # Properties. @@ -130,10 +146,7 @@ def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_Etherne def make(self, dst: 'str | bytes | bytearray' = '00:00:00:00:00:00', src: 'str | bytes | bytearray' = '00:00:00:00:00:00', - type: 'Enum_EtherType | StdlibEnum | AenumEnum | str | int' = Enum_EtherType.Internet_Protocol_version_4, - type_default: 'Optional[int]' = None, - type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - type_reversed: 'bool' = False, + type: 'Enum_EtherType | int' = Enum_EtherType.Internet_Protocol_version_4, payload: 'bytes | ProtocolBase | Schema' = b'', **kwargs: 'Any') -> 'Schema_Ethernet': """Make (construct) packet data. @@ -142,9 +155,6 @@ def make(self, dst: Destination MAC address. src: Source MAC address. type: EtherType. - type_default: Default EtherType. - type_namespace: EtherType namespace. - type_reversed: Whether EtherType is reversed. payload: Payload data. **kwargs: Arbitrary keyword arguments. @@ -153,16 +163,17 @@ def make(self, Raises: ProtocolError: If ``dst`` or ``src`` is rejected by - :meth:`~pcapkit.protocols.protocol.Protocol._make_mac_addr`. + :meth:`~pcapkit.protocols.protocol.Protocol._make_mac_addr`, + or ``type`` is a name, a :obj:`bool`, or a member or code its + field cannot carry. """ - _type = self._make_index(type, type_default, namespace=type_namespace, - reversed=type_reversed, pack=False) + _type = self._make_index(type, Enum_EtherType, bits=16, name='EtherType') return Schema_Ethernet( dst=self._make_mac_addr(dst), src=self._make_mac_addr(src), - type=_type, # type: ignore[arg-type] + type=_type, payload=payload, ) diff --git a/pcapkit/protocols/link/gre.py b/pcapkit/protocols/link/gre.py index b470ebd0ff..1f3a26ad02 100644 --- a/pcapkit/protocols/link/gre.py +++ b/pcapkit/protocols/link/gre.py @@ -81,10 +81,8 @@ from pcapkit.utilities.exceptions import ProtocolError if TYPE_CHECKING: - from enum import IntEnum as StdlibEnum - from typing import Any, Optional, Type + from typing import Any, Optional - from aenum import IntEnum as AenumEnum from typing_extensions import Literal from pcapkit.protocols.protocol import ProtocolBase @@ -245,10 +243,7 @@ def make(self, sequence_number: 'Optional[int]' = None, reserved: 'int' = 0, version: 'Literal[0]' = 0, - protocol_type: 'Enum_EtherType | StdlibEnum | AenumEnum | str | int' = Enum_EtherType.Internet_Protocol_version_4, # pylint: disable=line-too-long - protocol_type_default: 'Optional[int]' = None, - protocol_type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - protocol_type_reversed: 'bool' = False, + protocol_type: 'Enum_EtherType | int' = Enum_EtherType.Internet_Protocol_version_4, payload: 'bytes | ProtocolBase | Schema' = b'', **kwargs: 'Any') -> 'Schema_GRE': """Make (construct) packet data. @@ -281,9 +276,6 @@ def make(self, version: Version Number; :rfc:`2784` fixes this at ``0``. protocol_type: Protocol Type: the EtherType of the encapsulated payload. - protocol_type_default: Default value of ``protocol_type``. - protocol_type_namespace: Namespace of ``protocol_type``. - protocol_type_reversed: Reversed namespace of ``protocol_type``. payload: Payload data. **kwargs: Arbitrary keyword arguments. @@ -293,15 +285,15 @@ def make(self, Raises: ProtocolError: If ``checksum`` or ``reserved1`` is given with ``C`` clear; if ``reserved`` sets a bit outside ``0x4FF8``; if ``version`` - does not fit in 3 bits; or if ``checksum``, ``key`` or - ``sequence_number`` is given but does not fit the width of - its field (16 bits for ``checksum``, 32 bits for ``key`` and - ``sequence_number``). + does not fit in 3 bits; if ``protocol_type`` is a name, a + :obj:`bool`, or a member or code its field cannot carry; or if + ``checksum``, ``key`` or ``sequence_number`` is given but does + not fit the width of its field (16 bits for ``checksum``, 32 + bits for ``key`` and ``sequence_number``). """ - protocol_type_ = self._make_index(protocol_type, protocol_type_default, - namespace=protocol_type_namespace, - reversed=protocol_type_reversed, pack=False) + protocol_type_ = self._make_index(protocol_type, Enum_EtherType, bits=16, + name='protocol type') if checksum_present is None: checksum_present = checksum is not None @@ -338,7 +330,7 @@ def make(self, 'reserved_2': (reserved >> 3) & 0x1FF, 'version': version, }, - protocol_type=protocol_type_, # type: ignore[arg-type] + protocol_type=protocol_type_, checksum=0 if checksum_present and checksum is None else checksum, reserved1=reserved1, key=key, diff --git a/pcapkit/protocols/link/ieee802_11.py b/pcapkit/protocols/link/ieee802_11.py index 952724f8b8..ddc1a2d299 100644 --- a/pcapkit/protocols/link/ieee802_11.py +++ b/pcapkit/protocols/link/ieee802_11.py @@ -75,10 +75,8 @@ from pcapkit.utilities.exceptions import ProtocolError if TYPE_CHECKING: - from enum import IntEnum as StdlibEnum from typing import Any, Mapping, Optional, Type, Union - from aenum import IntEnum as AenumEnum from typing_extensions import Literal from pcapkit.protocols.protocol import ProtocolBase @@ -196,14 +194,8 @@ def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_IEEE802 return self._decode_next_layer(ieee802_11, -1, max(0, length - hdr_len)) def make(self, - type: 'Enum_Type | StdlibEnum | AenumEnum | str | int' = Enum_Type.Data, # pylint: disable=redefined-builtin - type_default: 'Optional[int]' = None, - type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - type_reversed: 'bool' = False, - subtype: 'Subtype | StdlibEnum | AenumEnum | str | int' = 0, - subtype_default: 'Optional[int]' = None, - subtype_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - subtype_reversed: 'bool' = False, + type: 'Enum_Type | int' = Enum_Type.Data, # pylint: disable=redefined-builtin + subtype: 'Subtype | int' = 0, version: 'Literal[0]' = 0, to_ds: 'Optional[bool]' = None, from_ds: 'Optional[bool]' = None, @@ -234,14 +226,8 @@ def make(self, Args: type: Type. - type_default: Default value of ``type``. - type_namespace: Namespace of ``type``. - type_reversed: Reversed namespace of ``type``. - subtype: Subtype; a name resolves against the registry ``type`` - selects unless ``subtype_namespace`` is given. - subtype_default: Default value of ``subtype``. - subtype_namespace: Namespace of ``subtype``. - subtype_reversed: Reversed namespace of ``subtype``. + subtype: Subtype, a member or code of the registry ``type`` + selects. version: Protocol Version; only ``0`` is built. to_ds: To DS. from_ds: From DS. @@ -274,18 +260,14 @@ def make(self, Constructed packet data. Raises: - ProtocolError: If a value does not fit its field, if ``version`` - is not ``0``, or if a field is given that the header does not - carry. + ProtocolError: If a value does not fit its field, if ``type`` or + ``subtype`` is a name or a member or code its field cannot + carry, if ``version`` is not ``0``, or if a field is given that + the header does not carry. """ - type_ = self._make_index(type, type_default, namespace=type_namespace, - reversed=type_reversed, pack=False) - type_ = self._make_uint(type_, bits=2, name='type') - subtype_ = self._make_index(subtype, subtype_default, - namespace=subtype_namespace or SUBTYPE[Enum_Type(type_)], - reversed=subtype_reversed, pack=False) - subtype_ = self._make_uint(subtype_, bits=4, name='subtype') + type_ = self._make_index(type, Enum_Type, bits=2, name='type') + subtype_ = self._make_index(subtype, SUBTYPE[type_], bits=4, name='subtype') frame_control = self._make_frame_control( version=version, type=type_, subtype=subtype_, diff --git a/pcapkit/protocols/link/l2tpv2.py b/pcapkit/protocols/link/l2tpv2.py index 2783b33433..8ba910f435 100644 --- a/pcapkit/protocols/link/l2tpv2.py +++ b/pcapkit/protocols/link/l2tpv2.py @@ -61,10 +61,8 @@ from pcapkit.utilities.exceptions import ProtocolError if TYPE_CHECKING: - from enum import IntEnum as StdlibEnum - from typing import Any, Optional, Type + from typing import Any, Optional - from aenum import IntEnum as AenumEnum from typing_extensions import Literal from pcapkit.protocols.protocol import ProtocolBase @@ -99,6 +97,22 @@ class L2TPv2(L2TP[Data_L2TP, Schema_L2TP], :class:`~pcapkit.protocols.link.l2tp.L2TP` carried, and refuses a version nibble other than ``2`` (:issue:`548`). + .. versionchanged:: 1.6.0 + + ``type`` of :meth:`make` takes a member or an :class:`int` code, + resolved with the enumeration's ``get``; a name, or a code the + one-bit field cannot carry, raises + :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than + resolving against an enumeration inferred from the caller's frame + (:issue:`2594`). + + .. versionremoved:: 1.6.0 + + The ``type_default``, ``type_namespace`` and ``type_reversed`` + arguments of :meth:`make`; resolve a name with + :meth:`~pcapkit.protocols.protocol.Protocol.resolve_index` first + (:issue:`2594`). + Note: IANA protocol number 115 (``L2TP``) is deliberately left unbound. It references :rfc:`3931`, i.e. **L2TPv3**, whose session and control @@ -258,10 +272,7 @@ def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_L2TP': def make(self, version: 'Literal[2]' = 2, - type: 'Enum_Type | StdlibEnum | AenumEnum | str | int' = Enum_Type.Data, - type_default: 'Optional[int]' = None, - type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - type_reversed: 'bool' = False, + type: 'Enum_Type | int' = Enum_Type.Data, priority: 'bool' = False, reserved: 'int' = 0, total_length: 'Optional[int]' = None, @@ -280,9 +291,6 @@ def make(self, Args: version: L2TP version. type: L2TP type. - type_default: Default value of type. - type_namespace: Namespace of type. - type_reversed: Reversed namespace of type. priority: Priority flag. reserved: Reserved bits of the flags word, in their on-wire positions (bits 2, 3, 5 and 8-11, i.e. within ``0x34F0``). @@ -309,13 +317,14 @@ def make(self, Constructed packet data. Raises: - ProtocolError: If ``total_length`` is given with ``length_flag`` - set to :data:`False`, if only one of ``ns`` and ``nr`` is - given, or if ``reserved`` sets a bit outside ``0x34F0``. + ProtocolError: If ``type`` is a name, or a member or code its + one-bit field cannot carry; if ``total_length`` is given with + ``length_flag`` set to :data:`False`; if only one of ``ns`` + and ``nr`` is given; or if ``reserved`` sets a bit outside + ``0x34F0``. """ - type_ = self._make_index(type, type_default, namespace=type_namespace, - reversed=type_reversed, pack=False) + type_ = self._make_index(type, Enum_Type, bits=1, name='type') # NOTE: :rfc:`2661` §3.1 makes ``S`` mark the presence of *both* Ns and # Nr, so a header cannot carry one without the other. Refuse a lone # value rather than drop it, or invent the other on the wire. diff --git a/pcapkit/protocols/link/linux_sll1.py b/pcapkit/protocols/link/linux_sll1.py index f40901e5b0..e247a07112 100644 --- a/pcapkit/protocols/link/linux_sll1.py +++ b/pcapkit/protocols/link/linux_sll1.py @@ -179,14 +179,16 @@ def make(self, Raises: ProtocolError: If ``link_addr`` does not fit in 8 octets, or - ``link_addr_length`` is given outside ``0`` to ``0xFFFF``. + ``link_addr_length`` is given outside ``0`` to ``0xFFFF``; or + if ``arphrd_type`` or ``protocol_type`` is a name, a + :obj:`bool`, or a member or code its field cannot carry. """ _addr, link_addr_length = _validate_link_addr( self.alias, link_addr, link_addr_length, max_length=0xFFFF) - _arphrd = self._make_index(arphrd_type, pack=False) - _proto = self._make_index(protocol_type, pack=False) + _arphrd = self._make_index(arphrd_type, Enum_Hardware, bits=16, name='ARPHRD type') + _proto = self._make_index(protocol_type, Enum_EtherType, bits=16, name='protocol type') return Schema_LinuxSLL1( packet_type=packet_type, diff --git a/pcapkit/protocols/link/linux_sll2.py b/pcapkit/protocols/link/linux_sll2.py index f27df79d81..bc0a9a9034 100644 --- a/pcapkit/protocols/link/linux_sll2.py +++ b/pcapkit/protocols/link/linux_sll2.py @@ -185,14 +185,16 @@ def make(self, Raises: ProtocolError: If ``link_addr`` does not fit in 8 octets, or - ``link_addr_length`` is given outside ``0`` to ``0xFF``. + ``link_addr_length`` is given outside ``0`` to ``0xFF``; or + if ``protocol_type`` or ``arphrd_type`` is a name, a + :obj:`bool`, or a member or code its field cannot carry. """ _addr, link_addr_length = _validate_link_addr( self.alias, link_addr, link_addr_length, max_length=0xFF) - _arphrd = self._make_index(arphrd_type, pack=False) - _proto = self._make_index(protocol_type, pack=False) + _arphrd = self._make_index(arphrd_type, Enum_Hardware, bits=16, name='ARPHRD type') + _proto = self._make_index(protocol_type, Enum_EtherType, bits=16, name='protocol type') return Schema_LinuxSLL2( protocol_type=_proto, diff --git a/pcapkit/protocols/link/vlan.py b/pcapkit/protocols/link/vlan.py index 6ffe5bdb6b..5b834f3b7d 100644 --- a/pcapkit/protocols/link/vlan.py +++ b/pcapkit/protocols/link/vlan.py @@ -59,10 +59,8 @@ from pcapkit.utilities.exceptions import UnsupportedCall if TYPE_CHECKING: - from enum import IntEnum as StdlibEnum - from typing import Any, NoReturn, Optional, Type + from typing import Any, NoReturn, Optional - from aenum import IntEnum as AenumEnum from typing_extensions import Literal from pcapkit.protocols.protocol import ProtocolBase @@ -96,6 +94,23 @@ class VLAN(Link[Data_VLAN, Schema_VLAN], # pylint: disable=abstract-method ``name``, ``alias`` and ``info_name``, moved to :class:`~pcapkit.protocols.link.c_tag.C_Tag`. + .. versionchanged:: 1.6.0 + + ``pcp`` and ``type`` of :meth:`make` each take a member or an + :class:`int` code, resolved with the enumeration's ``get``, and so does + the ``pcp`` of its ``tci``; a name, a :obj:`bool`, or a code the field + cannot carry raises :exc:`~pcapkit.utilities.exceptions.ProtocolError`, + rather than resolving against an enumeration inferred from the caller's + frame or writing a different value (:issue:`2594`). + + .. versionremoved:: 1.6.0 + + The ``pcp_default``, ``pcp_namespace``, ``pcp_reversed``, + ``type_default``, ``type_namespace`` and ``type_reversed`` arguments of + :meth:`make`; resolve a name with + :meth:`~pcapkit.protocols.protocol.Protocol.resolve_index` first + (:issue:`2594`). + """ ########################################################################## @@ -172,49 +187,39 @@ def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_VLAN': def make(self, tci: 'Optional[Schema_TCI | TCIType]' = None, - pcp: 'Enum_PriorityLevel | StdlibEnum | AenumEnum | str | int' = Enum_PriorityLevel.BE, - pcp_default: 'Optional[int]' = None, - pcp_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - pcp_reversed: 'bool' = False, + pcp: 'Enum_PriorityLevel | int' = Enum_PriorityLevel.BE, dei: 'bool' = False, vid: 'int' = 0, - type: 'Enum_EtherType | StdlibEnum | AenumEnum | str | int' = Enum_EtherType.Internet_Protocol_version_4, - type_default: 'Optional[int]' = None, - type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - type_reversed: 'bool' = False, + type: 'Enum_EtherType | int' = Enum_EtherType.Internet_Protocol_version_4, payload: 'bytes | ProtocolBase | Schema' = b'', **kwargs: 'Any') -> 'Schema_VLAN': """Make (construct) packet data. Args: - tci: TCI field. + tci: TCI field; its ``pcp``, ``dei`` and ``vid`` take the place of + the arguments of those names when given. pcp: Priority Code Point (PCP) field. - pcp_default: Default value of PCP field. - pcp_namespace: Namespace of PCP field. - pcp_reversed: Reversed flag of PCP field. dei: Drop Eligible Indicator (DEI) field. vid: VLAN Identifier (VID) field. type: EtherType field. - type_default: Default value of EtherType field. - type_namespace: Namespace of EtherType field. - type_reversed: Reversed flag of EtherType field. payload: Payload field. **kwargs: Arbitrary keyword arguments. Returns: Constructed packet data. + Raises: + ProtocolError: If ``pcp``, the ``pcp`` of ``tci``, or ``type`` is + a name, a :obj:`bool`, or a member or code its field cannot + carry. + """ if tci is not None: - pcp_value = tci['pcp'] + pcp = tci['pcp'] dei = tci['dei'] # type: ignore[assignment] vid = tci['vid'] - else: - pcp_value = self._make_index(pcp, pcp_default, namespace=pcp_namespace, - reversed=pcp_reversed, pack=False) - - type_value = self._make_index(type, type_default, namespace=type_namespace, - reversed=type_reversed, pack=False) + pcp_value = self._make_index(pcp, Enum_PriorityLevel, bits=3, name='PCP') + type_value = self._make_index(type, Enum_EtherType, bits=16, name='EtherType') return Schema_VLAN( tci={ @@ -222,7 +227,7 @@ def make(self, 'dei': dei, 'vid': vid, }, - type=type_value, # type: ignore[arg-type] + type=type_value, payload=payload, ) diff --git a/pcapkit/protocols/misc/checksum/ip_pseudo.py b/pcapkit/protocols/misc/checksum/ip_pseudo.py index e21a495152..3aa04a8f81 100644 --- a/pcapkit/protocols/misc/checksum/ip_pseudo.py +++ b/pcapkit/protocols/misc/checksum/ip_pseudo.py @@ -74,11 +74,9 @@ from pcapkit.utilities.exceptions import FieldValueError, ProtocolError, UnsupportedCall if TYPE_CHECKING: - from enum import IntEnum as StdlibEnum from ipaddress import IPv4Address, IPv6Address - from typing import IO, Any, NoReturn, Optional, Type + from typing import IO, Any, NoReturn, Optional - from aenum import IntEnum as AenumEnum from typing_extensions import Literal __all__ = ['IP_Pseudo', 'pseudo_header', 'verify_upper'] @@ -206,10 +204,7 @@ def make(self, ip: 'Optional[IPv4 | IPv6]' = None, src: 'Optional[IPv4Address | IPv6Address | str | int | bytes]' = None, dst: 'Optional[IPv4Address | IPv6Address | str | int | bytes]' = None, - protocol: 'Optional[Enum_TransType | StdlibEnum | AenumEnum | int | str]' = None, - protocol_default: 'Optional[int]' = None, - protocol_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - protocol_reversed: 'bool' = False, + protocol: 'Optional[Enum_TransType | int]' = None, upper_length: 'Optional[int]' = None, zero: 'bytes' = b'', **kwargs: 'Any') -> 'Schema_IP_Pseudo': @@ -229,10 +224,6 @@ def make(self, src: Source address; its IP version selects the layout. dst: Destination address, of the same IP version as ``src``. protocol: Upper-layer protocol number. - protocol_default: Default value of ``protocol``. - protocol_namespace: Namespace of ``protocol``; - :class:`~pcapkit.const.reg.transtype.TransType` by default. - protocol_reversed: Whether to reverse the namespace of ``protocol``. upper_length: Upper-layer packet length, header included. zero: Reserved octets, truncated or zero-filled to the layout's width. **kwargs: Arbitrary keyword arguments. @@ -242,8 +233,10 @@ def make(self, Raises: ProtocolError: If ``ip`` is neither IPv4 nor IPv6, a value is given - by neither the arguments nor ``ip``, or a header of ``ip`` is - malformed or not parsed. + by neither the arguments nor ``ip``, a header of ``ip`` is + malformed or not parsed, or ``protocol`` is a name, a + :obj:`bool`, or a member or code the 8-bit protocol field + cannot carry. FieldValueError: If ``src`` or ``dst`` is not a valid address, they differ in IP version, or ``upper_length`` does not fit the layout's length field. @@ -274,10 +267,7 @@ def make(self, if not 0 <= upper_length <= limit: raise FieldValueError(f'{self.alias}: upper-layer length {upper_length} ' f'out of range 0 to {limit:#x}') - if protocol_namespace is None: - protocol_namespace = Enum_TransType - proto = self._make_index(protocol, protocol_default, namespace=protocol_namespace, - reversed=protocol_reversed, pack=False) + proto = self._make_index(protocol, Enum_TransType, bits=8, name='protocol') # NOTE: Kept at the layout's width, so that a constructed data model # matches the parsed one. diff --git a/pcapkit/protocols/misc/pcap/header.py b/pcapkit/protocols/misc/pcap/header.py index 51fde30bb0..d41fc692f2 100644 --- a/pcapkit/protocols/misc/pcap/header.py +++ b/pcapkit/protocols/misc/pcap/header.py @@ -45,10 +45,8 @@ from pcapkit.utilities.exceptions import EndianError, UnsupportedCall if TYPE_CHECKING: - from enum import IntEnum as StdlibEnum - from typing import IO, Any, NoReturn, Optional, Type + from typing import IO, Any, NoReturn, Optional - from aenum import IntEnum as AenumEnum from typing_extensions import Literal, Self __all__ = ['Header'] @@ -62,7 +60,25 @@ class Header(ProtocolBase[Data_Header, Schema_Header], schema=Schema_Header, data=Data_Header): - """PCAP file global header extractor.""" + """PCAP file global header extractor. + + .. versionchanged:: 1.6.0 + + The ``network`` field of :meth:`make` takes a member or an :class:`int` + code, resolved with the enumeration's ``get``; a name, a :obj:`bool`, + or a code the field cannot carry raises + :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than + resolving against an enumeration inferred from the caller's frame or + writing a different value (:issue:`2594`). + + .. versionremoved:: 1.6.0 + + The ``network_default``, ``network_namespace`` and ``network_reversed`` + arguments of :meth:`make`; resolve a name with + :meth:`~pcapkit.protocols.protocol.Protocol.resolve_index` first + (:issue:`2594`). + + """ ########################################################################## # Properties. @@ -183,10 +199,7 @@ def make(self, version_minor: 'Optional[int]' = None, thiszone: int = 0, sigfigs: int = 0, snaplen: int = 0x40_000, - network: 'Enum_LinkType | StdlibEnum | AenumEnum | str | int' = Enum_LinkType.NULL, - network_default: 'Optional[int]' = None, - network_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, - network_reversed: bool = False, + network: 'Enum_LinkType | int' = Enum_LinkType.NULL, **kwargs: 'Any') -> 'Schema_Header': """Make (construct) packet data. @@ -202,14 +215,15 @@ def make(self, sigfigs: accuracy of timestamps snaplen: max length of captured packets, in octets network: data link type - network_default: default value for unknown data link type - network_namespace: data link type namespace - network_reversed: if namespace is ``str -> int`` pairs **kwargs: Arbitrary keyword arguments. Returns: Constructed packet data. + Raises: + ProtocolError: If ``network`` is a name, a :obj:`bool`, or a + member or code the 32-bit ``network`` field cannot carry. + """ # make magic number magic_number, lilendian = self._make_magic(byteorder, lilendian, bigendian, nanosecond) @@ -226,8 +240,7 @@ def make(self, thiszone=thiszone, sigfigs=sigfigs, snaplen=snaplen, - network=self._make_index(network, network_default, namespace=network_namespace, - reversed=network_reversed, pack=False), + network=self._make_index(network, Enum_LinkType, bits=32, name='network', alias='PCAP'), ) @classmethod diff --git a/pcapkit/protocols/misc/pcapng.py b/pcapkit/protocols/misc/pcapng.py index 28721b441b..bf7da11564 100644 --- a/pcapkit/protocols/misc/pcapng.py +++ b/pcapkit/protocols/misc/pcapng.py @@ -223,12 +223,10 @@ from datetime import datetime as dt_type from datetime import timedelta, timezone from decimal import Decimal - from enum import IntEnum as StdlibEnum from ipaddress import IPv4Address, IPv4Interface, IPv6Address, IPv6Interface from typing import (IO, Any, Callable, ClassVar, Counter, DefaultDict, Optional, Tuple, Type, TypeVar, Union) - from aenum import IntEnum as AenumEnum from mypy_extensions import DefaultArg, KwArg, NamedArg from typing_extensions import Literal @@ -331,27 +329,31 @@ def _unassigned_member(enum_cls: 'Type[_FlagsEnum]', value: 'int') -> '_FlagsEnu return obj -def _block_type_name(block_type: 'Enum_BlockType') -> 'str': +def _block_type_name(block_type: 'Enum_BlockType | int') -> 'str': """Return the name of a block type, for an option's placement message. Args: - block_type: The block type to name. + block_type: The block type to name, a member or a plain :obj:`int` + code. Returns: - The block type's name, e.g. ``Enhanced_Packet_Block``. + The block type's name, e.g. ``Enhanced_Packet_Block``; ``Block`` and + the code, e.g. ``Block 256``, for a code no member holds. Note: ``block_type`` is looked up rather than having its :attr:`~enum.Enum.name` - read directly, because :attr:`PCAPNG._type ` is only an - :class:`~pcapkit.const.pcapng.block_type.BlockType` on the parse path. - On the make path :meth:`~pcapkit.protocols.protocol.Protocol._make_index` - returns a plain :obj:`int`, which the assignment in - :meth:`PCAPNG.make ` only - :func:`~typing.cast` to the enumeration, so reading ``.name`` off it - there raises :exc:`AttributeError` (:issue:`2301`). + read directly, because :attr:`PCAPNG._type ` is not + always a member on the make path (:issue:`2301`). + :meth:`PCAPNG.make ` assigns + what :meth:`~pcapkit.protocols.protocol.Protocol._make_index` resolves + the code to: its member, or the plain :obj:`int` for a code in range + that no member holds (:issue:`2594`). """ - return Enum_BlockType(block_type).name + try: + return Enum_BlockType(block_type).name + except ValueError: + return f'Block {int(block_type)}' class PacketDirection(EnumLookup, enum.IntEnum): @@ -718,6 +720,29 @@ class PCAPNG(ProtocolBase[Data_PCAPNG, Schema_PCAPNG], block's section ``Context``, which ``context`` returned through 1.4.x, is :attr:`section_context` (:issue:`2172`). + .. versionchanged:: 1.6.0 + + Each enumeration field of :meth:`make` and of the block and option + makers, and the code of each option and the type of each name + resolution record given to a block maker, takes a member or an + :class:`int` code, resolved with the enumeration's ``get``, an option + code in the block's namespace; a name, a :obj:`bool` for a field wider + than one bit, or a code the field cannot carry raises + :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than + resolving against an enumeration inferred from the caller's frame, + writing a different value or raising another exception + (:issue:`2594`). + + .. versionremoved:: 1.6.0 + + The ``type_default``, ``type_namespace`` and ``type_reversed`` + arguments of :meth:`make`, and the ``_default``, ``_namespace`` and + ``_reversed`` arguments that went with each enumeration field of the + block and option makers -- ``linktype``, ``secrets_type``, ``filter``, + ``direction``, ``reception``, ``algorithm`` and ``verdict``; resolve a + name with :meth:`~pcapkit.protocols.protocol.Protocol.resolve_index` + first (:issue:`2594`). + """ if TYPE_CHECKING: @@ -1481,19 +1506,13 @@ def read(self, length: 'Optional[int]' = None, *, _read: 'bool' = True, return block def make(self, - type: 'Enum_BlockType | StdlibEnum | AenumEnum | str | int' = Enum_BlockType.Simple_Packet_Block, - type_default: 'Optional[int]' = None, - type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - type_reversed: 'bool' = False, + type: 'Enum_BlockType | int' = Enum_BlockType.Simple_Packet_Block, block: 'bytes | Data_PCAPNG | Schema_BlockType | dict[str, Any]' = b'', **kwargs: 'Any') -> 'Schema_PCAPNG': """Make PCAP-NG block data. Args: type: Block type. - type_default: Default block type. - type_namespace: Block type namespace. - type_reversed: Whether to reverse block type namespace. block: Block data. **kwargs: Arbitrary keyword arguments. @@ -1501,15 +1520,15 @@ def make(self, Constructed packet data. Raises: - ProtocolError: If ``block`` is none of the forms this maker reads -- - raw :obj:`bytes`, a :obj:`dict` of block arguments, a + ProtocolError: If ``type`` is a name, a :obj:`bool`, or a member + or code the 32-bit Block Type field cannot carry; or if + ``block`` is none of the forms this maker reads -- raw + :obj:`bytes`, a :obj:`dict` of block arguments, a :class:`~pcapkit.protocols.data.misc.pcapng.PCAPNG` data model or a :class:`~pcapkit.protocols.schema.schema.Schema`. """ - type_val = cast('Enum_BlockType', - self._make_index(type, type_default, namespace=type_namespace, - reversed=type_reversed, pack=False)) + type_val = self._make_index(type, Enum_BlockType, bits=32, name='block type', alias='PCAP-NG') self._type = type_val # NOTE: An option or record handed over as raw octets has its type code @@ -4497,7 +4516,7 @@ def _make_block_shb(self, block: 'Optional[Data_SectionHeaderBlock]' = None, *, major_version: 'Optional[int]' = None, minor_version: 'Optional[int]' = None, section_length: 'int' = -1, - options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long byteorder: 'Literal["big", "little"]' = sys.byteorder, **kwargs: 'Any') -> 'Schema_SectionHeaderBlock': """Make PCAP-NG section header block (SHB). @@ -4571,21 +4590,15 @@ def _make_block_shb(self, block: 'Optional[Data_SectionHeaderBlock]' = None, *, ) def _make_block_idb(self, block: 'Optional[Data_InterfaceDescriptionBlock]' = None, *, - linktype: 'Enum_LinkType | StdlibEnum | AenumEnum | str | int' = Enum_LinkType.NULL, - linktype_default: 'Optional[int]' = None, - linktype_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - linktype_reversed: 'bool' = False, + linktype: 'Enum_LinkType | int' = Enum_LinkType.NULL, snaplen: 'int' = 0, - options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long **kwargs: 'Any') -> 'Schema_InterfaceDescriptionBlock': """Make PCAP-NG interface description block (IDB). Args: block: Block data model. linktype: Link layer protocol type. - linktype_default: Default value of link layer protocol type. - linktype_namespace: Namespace of link layer protocol type. - linktype_reversed: Reversed flag for link layer protocol type namespace. snaplen: Snap length, an unsigned 32-bit value; 0, the default, means no limit. options: Block options. @@ -4595,9 +4608,11 @@ def _make_block_idb(self, block: 'Optional[Data_InterfaceDescriptionBlock]' = No Constructed block schema. Raises: - ProtocolError: If ``snaplen`` is not an :obj:`int` (a :obj:`bool` - included), or is outside ``0`` to ``0xFFFFFFFF``, which the - 32-bit SnapLen field would truncate (:issue:`2278`). + ProtocolError: If ``linktype`` is a name, a :obj:`bool`, or a + member or code the 16-bit LinkType field cannot carry; or if + ``snaplen`` is not an :obj:`int` (a :obj:`bool` included), or + is outside ``0`` to ``0xFFFFFFFF``, which the 32-bit SnapLen + field would truncate (:issue:`2278`). .. versionchanged:: 1.5.0 @@ -4622,8 +4637,8 @@ def _make_block_idb(self, block: 'Optional[Data_InterfaceDescriptionBlock]' = No snaplen = block.snaplen options = self._block_area(block, 'options') else: - linktype_val = self._make_index(linktype, linktype_default, namespace=linktype_namespace, # type: ignore[assignment] - reversed=linktype_reversed, pack=False) + linktype_val = self._make_index(linktype, Enum_LinkType, bits=16, name='[IDB] linktype', + alias='PCAP-NG') snaplen = self._make_uint(snaplen, bits=32, name='[IDB] snaplen', alias='PCAP-NG') if options is not None: @@ -4646,7 +4661,7 @@ def _make_block_epb(self, block: 'Optional[Data_EnhancedPacketBlock]' = None, *, captured_len: 'Optional[int]' = None, original_len: 'Optional[int]' = None, packet_data: 'bytes | ProtocolBase | Schema' = b'', - options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long **kwargs: 'Any') -> 'Schema_EnhancedPacketBlock': """Make PCAP-NG enhanced packet block (EPB). @@ -4754,8 +4769,8 @@ def _make_block_spb(self, block: 'Optional[Data_SimplePacketBlock]' = None, *, ), block, 'padding', 'padding_data') def _make_block_nrb(self, block: 'Optional[Data_NameResolutionBlock]' = None, *, - records: 'Optional[Record | list[Schema_NameResolutionRecord | tuple[Enum_RecordType, dict[str, Any]] | bytes]]' = None, - options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType, dict[str, Any]] | bytes]]' = None, + records: 'Optional[Record | list[Schema_NameResolutionRecord | tuple[Enum_RecordType | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long + options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long **kwargs: 'Any') -> 'Schema_NameResolutionBlock': """Make PCAP-NG name resolution block (NRB). @@ -4794,7 +4809,7 @@ def _make_block_nrb(self, block: 'Optional[Data_NameResolutionBlock]' = None, *, def _make_block_isb(self, block: 'Optional[Data_InterfaceStatisticsBlock]' = None, *, interface_id: 'int' = 0, timestamp: 'Optional[dt_type | int | float | Decimal]' = None, - options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long **kwargs: 'Any') -> 'Schema_InterfaceStatisticsBlock': """Make PCAP-NG interface statistics block (ISB). @@ -4871,21 +4886,15 @@ def _make_block_systemd(self, block: 'Optional[Data_SystemdJournalExportBlock]' ) def _make_block_dsb(self, block: 'Optional[Data_DecryptionSecretsBlock]' = None, *, - secrets_type: 'Enum_SecretsType | StdlibEnum | AenumEnum | str | int' = Enum_SecretsType.TLS_Key_Log, - secrets_type_default: 'Optional[int]' = None, - secrets_type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - secrets_type_reversed: 'bool' = False, + secrets_type: 'Enum_SecretsType | int' = Enum_SecretsType.TLS_Key_Log, secrets_data: 'Schema_DSBSecrets | Data_DSBSecrets | bytes | dict[str, Any]' = b'', - options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long **kwargs: 'Any') -> 'Schema_DecryptionSecretsBlock': """Make PCAP-NG decryption secrets block (DSB). Args: block: Block data model. secrets_type: Decryption secrets type. - secrets_type_default: Default value of decryption secrets type. - secrets_type_namespace: Namespace of decryption secrets type. - secrets_type_reversed: Reversed flag for namespace of decryption secrets type. secrets_data: Decryption secrets data. Raw :obj:`bytes` are read as the Secrets Data of a parsed block are, through the schema registered for ``secrets_type`` (:issue:`2273`). @@ -4896,8 +4905,10 @@ def _make_block_dsb(self, block: 'Optional[Data_DecryptionSecretsBlock]' = None, Constructed block schema. Raises: - ProtocolError: If ``secrets_data`` is none of the forms this maker - reads -- raw :obj:`bytes`, a :obj:`dict` of secrets arguments, a + ProtocolError: If ``secrets_type`` is a name, a :obj:`bool`, or a + member or code the 32-bit Secrets Type field cannot carry; or + if ``secrets_data`` is none of the forms this maker reads -- + raw :obj:`bytes`, a :obj:`dict` of secrets arguments, a :class:`~pcapkit.protocols.data.misc.pcapng.DSBSecrets` data model or a :class:`~pcapkit.protocols.schema.schema.Schema`. @@ -4909,8 +4920,8 @@ def _make_block_dsb(self, block: 'Optional[Data_DecryptionSecretsBlock]' = None, declared = block.secrets_length options = self._block_area(block, 'options') else: - secrets_type_val = self._make_index(secrets_type, secrets_type_default, namespace=secrets_type_namespace, # type: ignore[assignment] - reversed=secrets_type_reversed, pack=False) + secrets_type_val = self._make_index(secrets_type, Enum_SecretsType, bits=32, + name='[DSB] secrets_type', alias='PCAP-NG') if isinstance(secrets_data, bytes): from pcapkit.protocols.schema.misc.pcapng import dsb_secrets_selector @@ -4986,7 +4997,7 @@ def _make_block_dsb(self, block: 'Optional[Data_DecryptionSecretsBlock]' = None, def _make_block_cb(self, block: 'Optional[Data_CustomBlock]' = None, *, pen: 'int' = 0, data: 'Schema | bytes' = b'', - options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long **kwargs: 'Any') -> 'Schema_CustomBlock': """Make PCAP-NG custom block (CB). @@ -5041,7 +5052,7 @@ def _make_block_packet(self, block: 'Optional[Data_PacketBlock]' = None, *, captured_len: 'Optional[int]' = None, original_len: 'Optional[int]' = None, packet_data: 'bytes | ProtocolBase | Schema' = b'', - options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType, dict[str, Any]] | bytes]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long **kwargs: 'Any') -> 'Schema_PacketBlock': """Make PCAP-NG packet block (obsolete). @@ -5123,17 +5134,23 @@ def _make_block_packet(self, block: 'Optional[Data_PacketBlock]' = None, *, length2=total_length + BLOCK_MINIMUM[Enum_BlockType.Packet_Block] + packet_len, ), block, 'padding_data') - def _make_pcapng_options(self, options: 'Option | list[Schema_Option | tuple[Enum_OptionType, dict[str, Any]] | bytes] | bytes', + def _make_pcapng_options(self, options: 'Option | list[Schema_Option | tuple[Enum_OptionType | int, dict[str, Any]] | bytes] | bytes', # pylint: disable=line-too-long namespace: 'str') -> 'tuple[list[Schema_Option | bytes], int]': """Make options for PCAP-NG. Args: - options: PCAP-NG options. + options: PCAP-NG options. An option's code is a member, kept as + given, or an :class:`int`, resolved in ``namespace``. namespace: Namespace of options. Returns: Tuple of options and total length of options. + Raises: + ProtocolError: If an option's code is a name, a :obj:`bool`, or a + code the 16-bit option code field cannot carry + (:issue:`2594`). + """ # an option area kept as captured is written back verbatim if isinstance(options, bytes): @@ -5162,7 +5179,11 @@ def _make_pcapng_options(self, options: 'Option | list[Schema_Option | tuple[Enu data = schema data_len = len(schema.pack()) else: - code, args = cast('tuple[Enum_OptionType, dict[str, Any]]', schema) + kind, args = cast('tuple[Enum_OptionType | int, dict[str, Any]]', schema) + # NOTE: a member is a str, which ``_make_index`` refuses, and a code is + # resolved in the block's namespace, as a raw option's is. + code = kind if isinstance(kind, Enum_OptionType) else Enum_OptionType.get( + self._make_uint(kind, bits=16, name='option code', alias='PCAP-NG'), namespace=namespace) if code == Enum_OptionType.opt_endofopt: # moved to the end, written once has_endofopt = True continue @@ -5189,7 +5210,9 @@ def _make_pcapng_options(self, options: 'Option | list[Schema_Option | tuple[Enu return options_list, total_length options_list = [] - for code, option in options.items(multi=True): + for key, option in options.items(multi=True): + code = key if isinstance(key, Enum_OptionType) else Enum_OptionType.get( + self._make_uint(key, bits=16, name='option code', alias='PCAP-NG'), namespace=namespace) if code == Enum_OptionType.opt_endofopt: # moved to the end, written once has_endofopt = True continue @@ -5629,10 +5652,7 @@ def _make_option_if_tzone(self, type: 'Enum_OptionType', option: 'Optional[Data_ ) def _make_option_if_filter(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_FilterOption]' = None, *, - filter: 'Enum_FilterType | StdlibEnum | AenumEnum | str | int' = Enum_FilterType(0), - filter_default: 'Optional[int]' = None, - filter_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - filter_reversed: 'bool' = False, + filter: 'Enum_FilterType | int' = Enum_FilterType(0), expression: 'bytes | str' = b'', **kwargs: 'Any') -> 'Schema_IF_FilterOption': """Make PCAP-NG ``if_filter`` option. @@ -5641,15 +5661,16 @@ def _make_option_if_filter(self, type: 'Enum_OptionType', option: 'Optional[Data type: Option type. option: Option data model. filter: Filter type. - filter_default: Default filter value. - filter_namespace: Filter namespace. - filter_reversed: Whether filter namespace is reversed. expression: Filter expression. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. + Raises: + ProtocolError: If ``filter`` is a name, a :obj:`bool`, or a member + or code the 8-bit filter type field cannot carry. + """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_filter] option must be in Interface Description Block, ' @@ -5662,8 +5683,8 @@ def _make_option_if_filter(self, type: 'Enum_OptionType', option: 'Optional[Data filter_val = option.code expr_val = option.expression else: - filter_val = self._make_index(filter, filter_default, namespace=filter_namespace, # type: ignore[assignment] - reversed=filter_reversed, pack=False) + filter_val = self._make_index(filter, Enum_FilterType, bits=8, name='[if_filter] filter', + alias='PCAP-NG') expr_val = expression if isinstance(expression, bytes) else expression.encode() return Schema_IF_FilterOption( @@ -5873,14 +5894,8 @@ def _make_option_if_rxspeed(self, type: 'Enum_OptionType', option: 'Optional[Dat ) def _make_option_epb_flags(self, type: 'Enum_OptionType', option: 'Optional[Data_EPB_FlagsOption]' = None, *, - direction: 'PacketDirection | StdlibEnum | AenumEnum | str | int' = PacketDirection.UNKNOWN, - direction_default: 'Optional[int]' = None, - direction_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - direction_reversed: 'bool' = False, - reception: 'PacketReception | StdlibEnum | AenumEnum | str | int' = PacketReception.UNKNOWN, - reception_default: 'Optional[int]' = None, - reception_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - reception_reversed: 'bool' = False, + direction: 'PacketDirection | int' = PacketDirection.UNKNOWN, + reception: 'PacketReception | int' = PacketReception.UNKNOWN, fcs_len: 'int' = 0, checksum_not_ready: 'bool' = False, checksum_valid: 'bool' = False, @@ -5901,13 +5916,7 @@ def _make_option_epb_flags(self, type: 'Enum_OptionType', option: 'Optional[Data type: Option type. option: Option data model. direction: Packet direction. - direction_default: Default value of packet direction. - direction_namespace: Namespace of packet direction. - direction_reversed: Whether to reverse packet direction namespace. reception: Packet reception. - reception_default: Default value of packet reception. - reception_namespace: Namespace of packet reception. - reception_reversed: Whether to reverse packet reception namespace. fcs_len: Length of FCS field, in bytes. checksum_not_ready: Whether the checksum is not ready. checksum_valid: Whether the checksum is valid. @@ -5926,6 +5935,11 @@ def _make_option_epb_flags(self, type: 'Enum_OptionType', option: 'Optional[Data Returns: Constructed option schema. + Raises: + ProtocolError: If ``direction`` or ``reception`` is a name, a + :obj:`bool`, or a member or code its 2-bit or 3-bit sub-field + cannot carry. + """ if self._type != Enum_BlockType.Enhanced_Packet_Block: raise ProtocolError(f'PCAP-NG: [epb_flags] option must be in Enhanced Packet Block, ' @@ -5951,10 +5965,10 @@ def _make_option_epb_flags(self, type: 'Enum_OptionType', option: 'Optional[Data preamble_error = option.preamble_error symbol_error = option.symbol_error else: - direction_val = self._make_index(direction, direction_default, namespace=direction_namespace, # type: ignore[assignment] - reversed=direction_reversed, pack=False) - reception_val = self._make_index(reception, reception_default, namespace=reception_namespace, # type: ignore[assignment] - reversed=reception_reversed, pack=False) + direction_val = self._make_index(direction, PacketDirection, bits=2, + name='[epb_flags] direction', alias='PCAP-NG') + reception_val = self._make_index(reception, PacketReception, bits=3, + name='[epb_flags] reception', alias='PCAP-NG') return Schema_EPB_FlagsOption( type=type, @@ -5979,10 +5993,7 @@ def _make_option_epb_flags(self, type: 'Enum_OptionType', option: 'Optional[Data ) def _make_option_epb_hash(self, type: 'Enum_OptionType', option: 'Optional[Data_EPB_HashOption]' = None, *, - algorithm: 'Enum_HashAlgorithm | StdlibEnum | AenumEnum | int | str' = Enum_HashAlgorithm.two_s_complement, - algorithm_default: 'Optional[int]' = None, - algorithm_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - algorithm_reversed: 'bool' = False, + algorithm: 'Enum_HashAlgorithm | int' = Enum_HashAlgorithm.two_s_complement, hash: 'bytes' = b'', **kwargs: 'Any') -> 'Schema_EPB_HashOption': """Make PCAP-NG ``epb_hash`` option. @@ -5991,15 +6002,16 @@ def _make_option_epb_hash(self, type: 'Enum_OptionType', option: 'Optional[Data_ type: Option type. option: Option data model. algorithm: Hash algorithm. - algorithm_default: Default value of hash algorithm. - algorithm_namespace: Namespace of hash algorithm. - algorithm_reversed: Whether to reverse hash algorithm namespace. hash: Hash value. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. + Raises: + ProtocolError: If ``algorithm`` is a name, a :obj:`bool`, or a + member or code the 8-bit hash algorithm field cannot carry. + """ if self._type != Enum_BlockType.Enhanced_Packet_Block: raise ProtocolError(f'PCAP-NG: [epb_hash] option must be in Enhanced Packet Block, ' @@ -6009,8 +6021,8 @@ def _make_option_epb_hash(self, type: 'Enum_OptionType', option: 'Optional[Data_ algo_val = option.algorithm hash = option.hash else: - algo_val = self._make_index(algorithm, algorithm_default, namespace=algorithm_namespace, # type: ignore[assignment] - reversed=algorithm_reversed, pack=False) + algo_val = self._make_index(algorithm, Enum_HashAlgorithm, bits=8, + name='[epb_hash] algorithm', alias='PCAP-NG') return Schema_EPB_HashOption( type=type, @@ -6113,10 +6125,7 @@ def _make_option_epb_queue(self, type: 'Enum_OptionType', option: 'Optional[Data ) def _make_option_epb_verdict(self, type: 'Enum_OptionType', option: 'Optional[Data_EPB_VerdictOption]' = None, *, - verdict: 'Enum_VerdictType | StdlibEnum | AenumEnum | str | int' = Enum_VerdictType.Hardware, - verdict_default: 'Optional[int]' = None, - verdict_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - verdict_reversed: 'bool' = False, + verdict: 'Enum_VerdictType | int' = Enum_VerdictType.Hardware, value: 'bytes' = b'', **kwargs: 'Any') -> 'Schema_EPB_VerdictOption': """Make PCAP-NG ``epb_verdict`` option. @@ -6125,15 +6134,16 @@ def _make_option_epb_verdict(self, type: 'Enum_OptionType', option: 'Optional[Da type: Option type. option: Option data model. verdict: Verdict type. - verdict_default: Default value for verdict. - verdict_namespace: Namespace for verdict. - verdict_reversed: Whether to reverse the namespace. value: Verdict value. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. + Raises: + ProtocolError: If ``verdict`` is a name, a :obj:`bool`, or a + member or code the 8-bit verdict type field cannot carry. + """ if self._type != Enum_BlockType.Enhanced_Packet_Block: raise ProtocolError(f'PCAP-NG: [epb_verdict] option must be in Enhanced Packet Block, ' @@ -6145,8 +6155,8 @@ def _make_option_epb_verdict(self, type: 'Enum_OptionType', option: 'Optional[Da verdict_val = option.verdict value = option.value else: - verdict_val = self._make_index(verdict, verdict_default, namespace=verdict_namespace, # type: ignore[assignment] - reversed=verdict_reversed, pack=False) + verdict_val = self._make_index(verdict, Enum_VerdictType, bits=8, + name='[epb_verdict] verdict', alias='PCAP-NG') return Schema_EPB_VerdictOption( type=type, @@ -6470,14 +6480,8 @@ def _make_option_isb_usrdeliv(self, type: 'Enum_OptionType', option: 'Optional[D ) def _make_option_pack_flags(self, type: 'Enum_OptionType', option: 'Optional[Data_PACK_FlagsOption]' = None, *, - direction: 'PacketDirection | StdlibEnum | AenumEnum | str | int' = PacketDirection.UNKNOWN, - direction_default: 'Optional[int]' = None, - direction_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - direction_reversed: 'bool' = False, - reception: 'PacketReception | StdlibEnum | AenumEnum | str | int' = PacketReception.UNKNOWN, - reception_default: 'Optional[int]' = None, - reception_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - reception_reversed: 'bool' = False, + direction: 'PacketDirection | int' = PacketDirection.UNKNOWN, + reception: 'PacketReception | int' = PacketReception.UNKNOWN, fcs_len: 'int' = 0, checksum_not_ready: 'bool' = False, checksum_valid: 'bool' = False, @@ -6498,13 +6502,7 @@ def _make_option_pack_flags(self, type: 'Enum_OptionType', option: 'Optional[Dat type: Option type. option: Option data model. direction: Packet direction. - direction_default: Default value of packet direction. - direction_namespace: Namespace of packet direction. - direction_reversed: Whether to reverse packet direction namespace. reception: Packet reception. - reception_default: Default value of packet reception. - reception_namespace: Namespace of packet reception. - reception_reversed: Whether to reverse packet reception namespace. fcs_len: Length of FCS field, in bytes. checksum_not_ready: Whether the checksum is not ready. checksum_valid: Whether the checksum is valid. @@ -6523,6 +6521,11 @@ def _make_option_pack_flags(self, type: 'Enum_OptionType', option: 'Optional[Dat Returns: Constructed option schema. + Raises: + ProtocolError: If ``direction`` or ``reception`` is a name, a + :obj:`bool`, or a member or code its 2-bit or 3-bit sub-field + cannot carry. + """ if self._type != Enum_BlockType.Packet_Block: raise ProtocolError(f'PCAP-NG: [pack_flags] option must be in Packet Block, ' @@ -6548,10 +6551,10 @@ def _make_option_pack_flags(self, type: 'Enum_OptionType', option: 'Optional[Dat preamble_error = option.preamble_error symbol_error = option.symbol_error else: - direction_val = self._make_index(direction, direction_default, namespace=direction_namespace, # type: ignore[assignment] - reversed=direction_reversed, pack=False) - reception_val = self._make_index(reception, reception_default, namespace=reception_namespace, # type: ignore[assignment] - reversed=reception_reversed, pack=False) + direction_val = self._make_index(direction, PacketDirection, bits=2, + name='[pack_flags] direction', alias='PCAP-NG') + reception_val = self._make_index(reception, PacketReception, bits=3, + name='[pack_flags] reception', alias='PCAP-NG') return Schema_PACK_FlagsOption( type=type, @@ -6576,10 +6579,7 @@ def _make_option_pack_flags(self, type: 'Enum_OptionType', option: 'Optional[Dat ) def _make_option_pack_hash(self, type: 'Enum_OptionType', option: 'Optional[Data_PACK_HashOption]' = None, *, - algorithm: 'Enum_HashAlgorithm | StdlibEnum | AenumEnum | int | str' = Enum_HashAlgorithm.two_s_complement, - algorithm_default: 'Optional[int]' = None, - algorithm_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - algorithm_reversed: 'bool' = False, + algorithm: 'Enum_HashAlgorithm | int' = Enum_HashAlgorithm.two_s_complement, hash: 'bytes' = b'', **kwargs: 'Any') -> 'Schema_PACK_HashOption': """Make PCAP-NG ``pack_hash`` option. @@ -6588,15 +6588,16 @@ def _make_option_pack_hash(self, type: 'Enum_OptionType', option: 'Optional[Data type: Option type. option: Option data model. algorithm: Hash algorithm. - algorithm_default: Default value of hash algorithm. - algorithm_namespace: Namespace of hash algorithm. - algorithm_reversed: Whether to reverse hash algorithm namespace. hash: Hash value. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. + Raises: + ProtocolError: If ``algorithm`` is a name, a :obj:`bool`, or a + member or code the 8-bit hash algorithm field cannot carry. + """ if self._type != Enum_BlockType.Packet_Block: raise ProtocolError(f'PCAP-NG: [pack_hash] option must be in Packet Block, ' @@ -6606,8 +6607,8 @@ def _make_option_pack_hash(self, type: 'Enum_OptionType', option: 'Optional[Data algo_val = option.algorithm hash = option.hash else: - algo_val = self._make_index(algorithm, algorithm_default, namespace=algorithm_namespace, # type: ignore[assignment] - reversed=algorithm_reversed, pack=False) + algo_val = self._make_index(algorithm, Enum_HashAlgorithm, bits=8, + name='[pack_hash] algorithm', alias='PCAP-NG') return Schema_PACK_HashOption( type=type, @@ -6616,7 +6617,7 @@ def _make_option_pack_hash(self, type: 'Enum_OptionType', option: 'Optional[Data data=hash, ) - def _make_nrb_records(self, records: 'Record | list[Schema_NameResolutionRecord | tuple[Enum_RecordType, dict[str, Any]] | bytes] | bytes') -> 'tuple[list[Schema_NameResolutionRecord | bytes], int]': + def _make_nrb_records(self, records: 'Record | list[Schema_NameResolutionRecord | tuple[Enum_RecordType | int, dict[str, Any]] | bytes] | bytes') -> 'tuple[list[Schema_NameResolutionRecord | bytes], int]': # pylint: disable=line-too-long """Make name resolution records for PCAP-NG. Args: @@ -6625,6 +6626,11 @@ def _make_nrb_records(self, records: 'Record | list[Schema_NameResolutionRecord Returns: Tuple of name resolution records and total length of the records. + Raises: + ProtocolError: If a record's type is a name, a :obj:`bool`, or a + code the 16-bit record type field cannot carry + (:issue:`2594`). + """ # a record area kept as captured is written back verbatim if isinstance(records, bytes): @@ -6652,7 +6658,8 @@ def _make_nrb_records(self, records: 'Record | list[Schema_NameResolutionRecord data = schema data_len = len(schema.pack()) else: - code, args = cast('tuple[Enum_RecordType, dict[str, Any]]', schema) + kind, args = cast('tuple[Enum_RecordType | int, dict[str, Any]]', schema) + code = self._make_index(kind, Enum_RecordType, bits=16, name='record type', alias='PCAP-NG') if code == Enum_RecordType.nrb_record_end: # moved to the end, written once has_record_end = True continue @@ -6678,7 +6685,8 @@ def _make_nrb_records(self, records: 'Record | list[Schema_NameResolutionRecord return records_list, total_length records_list = [] - for code, record in records.items(multi=True): + for key, record in records.items(multi=True): + code = self._make_index(key, Enum_RecordType, bits=16, name='record type', alias='PCAP-NG') if code == Enum_RecordType.nrb_record_end: # moved to the end, written once has_record_end = True continue diff --git a/pcapkit/protocols/protocol.py b/pcapkit/protocols/protocol.py index 370ad5512e..38c54e5df1 100644 --- a/pcapkit/protocols/protocol.py +++ b/pcapkit/protocols/protocol.py @@ -21,7 +21,6 @@ """ import abc -import ast import collections import collections.abc import contextlib @@ -74,6 +73,7 @@ from typing_extensions import Literal, Self from pcapkit.corekit.context import ProtocolContext + from pcapkit.corekit.enum import EnumLookup __all__ = ['Protocol', 'ProtocolBase'] @@ -81,40 +81,12 @@ _ST = TypeVar('_ST', bound='Schema') _CTX = TypeVar('_CTX', bound='ProtocolContext') _VT = TypeVar('_VT') +_ET = TypeVar('_ET', bound='EnumLookup') # readable characters' order list readable = [ord(char) for char in filter(lambda char: not char.isspace(), string.printable)] -@functools.lru_cache(maxsize=None) -def _make_index_calls(func: 'Callable[..., Any]') -> 'tuple[tuple[int, int, str], ...]': - """Locate the ``_make_index`` calls in the source of ``func``. - - Args: - func: method calling ``_make_index`` - - Returns: - One ``(first_line, last_line, argument)`` triple per call whose first - argument is a plain name, with absolute line numbers. Empty if the - source of ``func`` is unavailable. - - """ - try: - lines, start = inspect.getsourcelines(func) - tree = ast.parse(textwrap.dedent(''.join(lines))) - except (OSError, TypeError, SyntaxError): - return () - # NOTE: ``ast.parse`` always sets ``end_lineno`` on a call. - return tuple( - (node.lineno + start - 1, - cast('int', node.end_lineno) + start - 1, - node.args[0].id) - for node in ast.walk(tree) - if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) - and node.func.attr == '_make_index' and node.args and isinstance(node.args[0], ast.Name) - ) - - def _passes_raw(value: 'Any', nested: 'bool' = False) -> 'bool': """Tell if ``make()`` keywords may carry a raw :obj:`bytes` item. @@ -928,6 +900,62 @@ def unquote(url: str, *, encoding: 'str' = 'utf-8', except UnicodeError: return url.replace('%', r'\x').encode().decode('unicode_escape', errors='replace') + @staticmethod + def resolve_index(name: 'str | int | StdlibEnum | AenumEnum', default: 'Optional[int]' = None, *, + namespace: 'dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]', + reversed: 'bool' = False) -> 'int | StdlibEnum | AenumEnum': # pylint: disable=redefined-builtin + """Resolve a name for an enumeration field of :meth:`make`. + + A field of :meth:`make` that holds an enumeration takes a member or an + :class:`int` code, and refuses a name (:issue:`2594`). This resolves a + name against a ``namespace`` given explicitly, so that its result can + be passed on:: + + >>> from pcapkit.const.reg.ethertype import EtherType + >>> from pcapkit.protocols.link.ethernet import Ethernet + >>> code = Ethernet.resolve_index('Address_Resolution_Protocol', + ... namespace=EtherType) + >>> Ethernet(type=code).info.type + + + Args: + name: a member name; a member or an :class:`int` is returned as + it is, for :meth:`make` to resolve + default: the code to return when ``name`` is not in ``namespace`` + namespace: the enumeration ``name`` names a member of, or a + :obj:`dict` of codes and names + reversed: if ``namespace`` is a :obj:`dict` of names to codes, + rather than of codes to names + + Returns: + The member ``name`` names, for an enumeration ``namespace``; the + code it names, for a :obj:`dict`; else ``default``. + + Raises: + ProtocolNotImplemented: If ``name`` is a :obj:`str` that is not in + ``namespace`` and ``default`` is :data:`None`. + + .. versionadded:: 1.6.0 + + Takes over the ``_default``, ``_namespace`` and + ``_reversed`` arguments of :meth:`make` (:issue:`2594`). + + """ + if not isinstance(name, str): + return name + try: + if isinstance(namespace, type) and issubclass(namespace, (enum.Enum, aenum.Enum)): + return namespace[name] + if isinstance(namespace, dict): + if reversed: + return cast('dict[str, int]', namespace)[name] + return {value: key for key, value in cast('dict[int, str]', namespace).items()}[name] + raise KeyError(name) # caught below, so a non-mapping misses as an unknown name does + except KeyError as error: + if default is None: + raise ProtocolNotImplemented(f'{name!r} is not a name in {namespace!r}') from error + return default + @staticmethod def expand_comp(value: 'str | ProtocolBase | Type[ProtocolBase]') -> 'tuple': """Expand protocol class to protocol name. @@ -2356,211 +2384,57 @@ def _make_mac_addr(self, addr: 'str | bytes | bytearray', *, size: 'Optional[int return bytes.fromhex(b''.join(octets).decode()) raise ProtocolError(f'{prefix}: invalid {name}: {addr!r}') - @overload # pragma: no cover - @classmethod - def _make_index(cls, name: 'int | StdlibEnum | AenumEnum', *, pack: 'Literal[False]' = ...) -> 'int': ... - @overload # pragma: no cover - @classmethod - def _make_index(cls, name: 'int | StdlibEnum | AenumEnum', *, pack: 'Literal[True]', - size: 'int' = ..., signed: 'bool' = ..., lilendian: 'bool' = ...) -> 'bytes': ... - @overload # pragma: no cover - @classmethod - def _make_index(cls, name: 'str', default: 'Optional[int]' = ..., *, - namespace: 'Type[StdlibEnum] | Type[AenumEnum]', pack: 'Literal[False]' = ...) -> 'int': ... - @overload # pragma: no cover - @classmethod - def _make_index(cls, name: 'str', default: 'Optional[int]' = ..., *, - namespace: 'Type[StdlibEnum] | Type[AenumEnum]', pack: 'Literal[True]', - size: 'int' = ..., signed: 'bool' = ..., lilendian: 'bool' = ...) -> 'bytes': ... - @overload # pragma: no cover - @classmethod - def _make_index(cls, name: 'str', default: 'Optional[int]' = ..., *, namespace: 'dict[int, str]', - reversed: 'Literal[False]' = ..., # pylint: disable=redefined-builtin - pack: 'Literal[False]' = ...) -> 'int': ... - @overload # pragma: no cover - @classmethod - def _make_index(cls, name: 'str', default: 'Optional[int]' = ..., *, namespace: 'dict[int, str]', - reversed: 'Literal[False]' = ..., # pylint: disable=redefined-builtin - pack: 'Literal[True]', size: 'int' = ..., signed: 'bool' = ..., - lilendian: 'bool' = ...) -> 'bytes': ... - @overload # pragma: no cover - @classmethod - def _make_index(cls, name: 'str', default: 'Optional[int]' = ..., *, namespace: 'dict[str, int]', - reversed: 'Literal[True]', # pylint: disable=redefined-builtin - pack: 'Literal[False]' = ...) -> 'int': ... - @overload # pragma: no cover - @classmethod - def _make_index(cls, name: 'str', default: 'Optional[int]' = ..., *, namespace: 'dict[str, int]', - reversed: 'Literal[True]', # pylint: disable=redefined-builtin - pack: 'Literal[True]', size: 'int' = ..., signed: 'bool' = ..., - lilendian: 'bool' = ...) -> 'bytes': ... - @overload # pragma: no cover - @classmethod - def _make_index(cls, name: 'str | int | StdlibEnum | AenumEnum', default: 'Optional[int]' = ..., *, - namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = ..., - reversed: 'bool' = ..., pack: 'Literal[False]' = ...) -> 'int': ... + def _make_index(self, value: 'Any', namespace: 'Type[_ET]', *, bits: 'int', name: 'str', + alias: 'Optional[str]' = None) -> '_ET': + """Resolve the code given for an enumeration field of ``make()``. - @classmethod - def _make_index(cls, name: 'str | int | StdlibEnum | AenumEnum', default: 'Optional[int]' = None, *, - namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, - reversed: 'bool' = False, # pylint: disable=redefined-builtin - pack: 'bool' = False, size: 'int' = 4, signed: 'bool' = False, - lilendian: 'bool' = False) -> 'int | bytes': - """Return first index of ``name`` from a :obj:`dict` or enumeration. + The field takes a member of ``namespace`` or an :class:`int` code, + and either resolves with ``namespace``'s + :meth:`~pcapkit.corekit.enum.EnumLookup.get`. A name is refused rather + than looked up, so a caller holding one resolves it first, with + :meth:`resolve_index` (:issue:`2594`). The code is checked as + :meth:`_make_uint` checks an integer, so a :obj:`bool` passes only for + a one-bit field (:issue:`2617`): the field's width decides what is + refused, not the enumeration's range. - Arguments: - name: item to be indexed - default: default value - namespace: namespace for item - reversed: if namespace is ``str -> int`` pairs - pack: if need :func:`struct.pack` to pack the result - size: buffer size - signed: signed flag - lilendian: little-endian flag + Args: + value: the value given for the field + namespace: the enumeration the field holds + bits: width of the field, in bits + name: name of the field, for the error message + alias: prefix of the error message, for a caller whose + :attr:`alias` raises before ``read()`` sets it; :attr:`alias` + by default Returns: - Index of ``name`` from a dict or enumeration. If ``pack`` is - :data:`True`, returns :obj:`bytes`; otherwise, returns :obj:`int`. + What ``namespace``'s :meth:`~pcapkit.corekit.enum.EnumLookup.get` + resolves ``value`` to. For most registries that includes an + unassigned code, as the ``Unassigned`` or ``Reserved`` member their + ``_missing_`` builds. Where ``get`` raises :exc:`ValueError` for a + code the field carries, as a closed enumeration does, the code + itself comes back as an :class:`int`, so that the field is rebuilt + as captured. Raises: - ProtocolNotImplemented: If ``name`` is **NOT** in ``namespace`` - and ``default`` is :data:`None`. - StructError: If ``pack`` is :data:`True` and the index could not be - packed into ``size`` octets. - - Note: - If ``name`` is a :obj:`str` and ``namespace`` is :data:`None`, - the namespace is inferred from the calling ``make`` method: it - is the enumeration class of the default value of the argument - that ``name`` was passed as, found from the call in the - caller's source (see ``_make_index_namespace()``). + ProtocolError: If ``value`` is not an :class:`int` (a name among + them), is a :obj:`bool` for a field wider than one bit, is + negative, or does not fit in ``bits`` bits. The message names + ``alias``. - """ - if isinstance(name, (enum.Enum, aenum.Enum)): - index = cast('int', name.value) - elif isinstance(name, int): - index = name - else: # name is str - if namespace is None: - namespace = cls._make_index_namespace(name, inspect.currentframe()) - try: - if isinstance(namespace, type) and issubclass(namespace, (enum.IntEnum, aenum.IntEnum)): - index = cast('int', namespace[name].value) - elif isinstance(namespace, dict): - if reversed: - if TYPE_CHECKING: - namespace = cast('dict[str, int]', namespace) - index = namespace[name] - else: - if TYPE_CHECKING: - namespace = cast('dict[int, str]', namespace) - index = {v: k for k, v in namespace.items()}[name] - else: - # Caught by the handler below, so this never escapes: it jumps - # to the shared "name is not in namespace" path. A pcapkit - # exception here would log at CRITICAL for something the - # ``except`` below handles. - raise KeyError(name) - except KeyError as error: - if default is None: - raise ProtocolNotImplemented(f'protocol {name!r} not implemented') from error - index = default - - if pack: - return cls._make_pack(index, size=size, signed=signed, lilendian=lilendian) - return index - - @classmethod - def _make_index_namespace(cls, name: 'str', - frame: 'Optional[FrameType]') -> 'Optional[Type[StdlibEnum] | Type[AenumEnum]]': - """Infer the enumeration namespace of ``name`` for :meth:`_make_index`. - - The caller of :meth:`_make_index` (``frame.f_back``) is looked up - among the methods of ``cls``. An argument ``arg`` of that method - matches if it is accompanied by an ``arg_namespace`` argument that - is :data:`None`, its current value is a :obj:`str` equal to - ``name``, and its default - value is an :class:`~enum.IntEnum` (or ``aenum.IntEnum``) - member. The candidate namespace of a match is the class of that - default value. - - Only the argument the call passes as ``name`` is considered: the - caller's source is parsed (by ``_make_index_calls``) for the - :meth:`_make_index` call spanning the current line, and its first - argument names the method argument, so two arguments holding the - same member name each resolve against their own enumeration. Every - argument is considered when that call cannot be located (e.g. the - source is unavailable) or its first argument is not an argument of - the method (e.g. ``value = arg; self._make_index(value)``). - - Args: - name: member name given to :meth:`_make_index` - frame: frame of :meth:`_make_index` + .. versionchanged:: 1.6.0 - Returns: - The candidate namespace if exactly one argument matches. If - several match, any candidate, provided every candidate has - ``name`` as a member and maps it to the same value. - :data:`None` if the caller is not found, nothing matches, or no - candidate has ``name`` as a member. - - Raises: - ProtocolNotImplemented: If every argument is considered, several - arguments match and their candidates disagree on ``name``: - some lack it, or they map it to different values. + Takes the enumeration and the width of the field, in place of the + ``default``, ``namespace``, ``reversed`` and ``pack`` arguments, + refuses a code the field cannot carry, and refuses a name rather + than resolving it against an enumeration inferred from the calling + method's frame (:issue:`2594`). """ - caller = frame.f_back if frame is not None else None - if caller is None: - return None + code = self._make_uint(value, bits=bits, name=name, alias=alias) try: - code = caller.f_code - values = caller.f_locals - - func = None - for klass in cls.__mro__: - for attr in vars(klass).values(): - attr = getattr(attr, '__func__', attr) - if getattr(attr, '__code__', None) is code: - func = attr - break - if func is not None: - break - if func is None: - return None - - params = inspect.signature(func).parameters - lineno = caller.f_lineno - located = {arg for first, last, arg in _make_index_calls(func) if first <= lineno <= last} - found = {} # type: dict[str, Type[StdlibEnum] | Type[AenumEnum]] - for arg, param in params.items(): - # once the call is located, only its own argument is a candidate - if len(located) == 1 and located <= params.keys() and arg not in located: - continue - value = values.get(arg) - if f'{arg}_namespace' not in params or not isinstance(value, str) or value != name: - continue - # an explicit namespace resolves that argument by itself - if values.get(f'{arg}_namespace') is not None: - continue - if isinstance(param.default, (enum.IntEnum, aenum.IntEnum)): - found[arg] = type(param.default) - finally: - del caller, frame - - if len(found) == 1: - return next(iter(found.values())) - - known = {arg: int(space[name]) for arg, space in found.items() if name in space.__members__} - if not known: - return None - if len(known) < len(found) or len(set(known.values())) > 1: - matches = ', '.join(f'{space.__name__}.{name} = {known[arg]}' if arg in known - else f'{space.__name__} has no {name!r}' for arg, space in found.items()) - hint = ' or '.join(f'{arg}_namespace=' for arg in found) - raise ProtocolNotImplemented(f'{cls.__name__}: ambiguous member name {name!r} given to ' - f'{", ".join(found)} ({matches}); pass {hint} explicitly') - return next(iter(found.values())) + return namespace.get(code) + except ValueError: + return cast('_ET', code) @classmethod def _make_payload(cls, data: 'Data') -> 'ProtocolBase': diff --git a/pcapkit/protocols/transport/dccp.py b/pcapkit/protocols/transport/dccp.py index 49673d4dca..06acfc7e32 100644 --- a/pcapkit/protocols/transport/dccp.py +++ b/pcapkit/protocols/transport/dccp.py @@ -366,7 +366,7 @@ def make(self, service_code: 'int' = 0, reset_code: 'Enum_ResetCode | int' = Enum_ResetCode.Unspecified, reset_data: 'bytes' = bytes(3), - options: 'Optional[list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes] | Option]' = None, # pylint: disable=line-too-long + options: 'Optional[list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes] | Option]' = None, # pylint: disable=line-too-long payload: 'bytes | ProtocolBase | Schema' = b'', pseudo: 'Optional[IP_Pseudo]' = None, **kwargs: 'Any') -> 'Schema_DCCP': @@ -408,15 +408,17 @@ def make(self, Constructed packet data. Raises: - ProtocolError: If ``srcport`` or ``dstport`` is a :obj:`bool`, the - header with its options runs past the 1020 octets a Data Offset - can give, an option maker rejects its option, a numeric field - does not fit its width, ``checksum`` is not two octets, or - ``reset_data`` is not three. - EnumValueError: If ``type`` is out of its 4-bit range. + ProtocolError: If ``srcport`` or ``dstport`` is a :obj:`bool`, + ``type`` (4 bits), ``reset_code`` (8 bits) or the type of an + option in ``options`` (8 bits) is a name, a :obj:`bool`, or a + member or code that does not fit its field, + the header with its options runs past the 1020 octets a Data + Offset can give, an option maker rejects its option, a numeric + field does not fit its width, ``checksum`` is not two octets, + or ``reset_data`` is not three. """ - code = Enum_PacketType.get(type) + code = self._make_index(type, Enum_PacketType, bits=4, name='type') type_field = { 'reserved': self._make_uint(reserved, bits=3, name='reserved'), 'type': int(code), @@ -451,7 +453,7 @@ def make(self, ack_reserved=self._make_uint(ack_reserved, bits=(2 if x else 1) * 8, name='ack_reserved'), ack=self._make_uint(ack, bits=seq_bits, name='acknowledgement number'), service_code=self._make_uint(service_code, bits=32, name='service code'), - reset_code=reset_code, + reset_code=self._make_index(reset_code, Enum_ResetCode, bits=8, name='reset code'), reset_data=self._make_octets(reset_data, size=3, name='reset data'), options=options_value, payload=payload, @@ -574,7 +576,7 @@ def _read_dccp_options(self) -> 'Option': options.add(code, meth(schema, options=options)) return options - def _make_dccp_options(self, options: 'list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes] | Option') -> 'tuple[list[Schema_Option | bytes], int]': # pylint: disable=line-too-long + def _make_dccp_options(self, options: 'list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes] | Option') -> 'tuple[list[Schema_Option | bytes], int]': # pylint: disable=line-too-long """Make options for DCCP. Args: @@ -596,10 +598,12 @@ def _make_dccp_options(self, options: 'list[Schema_Option | tuple[Enum_Option, d if isinstance(item, (bytes, Schema)): options_list.append(item) else: - code, args = cast('tuple[Enum_Option, dict[str, Any]]', item) + kind, args = cast('tuple[Enum_Option | int, dict[str, Any]]', item) + code = self._make_index(kind, Enum_Option, bits=8, name='option type') options_list.append(self._option_maker(code)(code, **args)) else: - for code, option in options.items(multi=True): + for key, option in options.items(multi=True): + code = self._make_index(key, Enum_Option, bits=8, name='option type') options_list.append(self._option_maker(code)(code, option)) total_length = sum(len(item) if isinstance(item, bytes) else len(item.pack()) @@ -1112,7 +1116,9 @@ def _make_opt_change(self, code: 'Enum_Option', opt: 'Optional[Data_Change]' = N Constructed option schema. Raises: - ProtocolError: If ``value`` is given without a ``feature``. + ProtocolError: If ``value`` is given without a ``feature``, or + ``feature`` is a name, a :obj:`bool`, or a member or code that + does not fit 8 bits. """ if opt is not None: @@ -1129,7 +1135,7 @@ def _make_opt_change(self, code: 'Enum_Option', opt: 'Optional[Data_Change]' = N return Schema_Change( type=code, length=self._option_length(code, len(value) + 3), - feature=feature, + feature=self._make_index(feature, Enum_Feature, bits=8, name=f'[Opt {code}] feature'), value=value, ) @@ -1151,7 +1157,9 @@ def _make_opt_confirm(self, code: 'Enum_Option', opt: 'Optional[Data_Confirm]' = Constructed option schema. Raises: - ProtocolError: If ``value`` is given without a ``feature``. + ProtocolError: If ``value`` is given without a ``feature``, or + ``feature`` is a name, a :obj:`bool`, or a member or code that + does not fit 8 bits. """ if opt is not None: @@ -1168,7 +1176,7 @@ def _make_opt_confirm(self, code: 'Enum_Option', opt: 'Optional[Data_Confirm]' = return Schema_Confirm( type=code, length=self._option_length(code, len(value) + 3), - feature=feature, + feature=self._make_index(feature, Enum_Feature, bits=8, name=f'[Opt {code}] feature'), value=value, ) @@ -1245,8 +1253,9 @@ def _make_opt_ack_vector(self, code: 'Enum_Option', opt: 'Optional[Data_AckVecto Constructed option schema. Raises: - ProtocolError: If a state does not fit 2 bits or a run length - 6 bits. + ProtocolError: If a state is a name, a :obj:`bool`, or a member or + code that does not fit 2 bits, or a run length does not fit 6 + bits. """ if opt is not None: @@ -1255,7 +1264,8 @@ def _make_opt_ack_vector(self, code: 'Enum_Option', opt: 'Optional[Data_AckVecto octets = bytearray() for run in vector: state, run_length = (run.state, run.run_length) if isinstance(run, Data_AckVectorRun) else run - if not (0 <= state <= 0x03 and 0 <= run_length <= 0x3F): + state = self._make_index(state, Enum_AckVectorState, bits=2, name=f'[Opt {code}] Ack Vector state') + if not 0 <= run_length <= 0x3F: raise ProtocolError(f'{self.alias}: [Opt {code}] invalid Ack Vector run ' f'({state}, {run_length})') octets.append((state << 6) | run_length) @@ -1285,8 +1295,9 @@ def _make_opt_data_dropped(self, code: 'Enum_Option', opt: 'Optional[Data_DataDr Raises: ProtocolError: If a run length does not fit 7 bits in a normal - block, or a drop code 3 bits or a run length 4 bits in a drop - block. + block, or in a drop block a run length does not fit 4 bits or + the drop code is a name, a :obj:`bool`, or a member or code that + does not fit 3 bits. """ if opt is not None: @@ -1295,9 +1306,11 @@ def _make_opt_data_dropped(self, code: 'Enum_Option', opt: 'Optional[Data_DataDr octets = bytearray() for block in vector: drop, run_length = (block.code, block.run_length) if isinstance(block, Data_DataDroppedBlock) else block + if drop is not None: + drop = self._make_index(drop, Enum_DropCode, bits=3, name=f'[Opt {code}] drop code') if drop is None and 0 <= run_length <= 0x7F: octets.append(run_length) - elif drop is not None and 0 <= drop <= 0x07 and 0 <= run_length <= 0x0F: + elif drop is not None and 0 <= run_length <= 0x0F: octets.append(0x80 | (drop << 4) | run_length) else: raise ProtocolError(f'{self.alias}: [Opt {code}] invalid Data Dropped block ' diff --git a/pcapkit/protocols/transport/sctp.py b/pcapkit/protocols/transport/sctp.py index 82cfac1c7a..f4a82f326d 100644 --- a/pcapkit/protocols/transport/sctp.py +++ b/pcapkit/protocols/transport/sctp.py @@ -455,6 +455,16 @@ class SCTP(Transport[Data_SCTP, Schema_SCTP], :meth:`~pcapkit.protocols.transport.transport.Transport.from_data` as captured, where it lost the octets after its last whole chunk (:issue:`2673`). + * The type of each chunk given to :meth:`make`, the type of each + parameter and the code of each error cause given to a chunk maker, + and each enumeration field of the chunk, parameter and error cause + makers -- a DATA chunk's payload protocol identifier, and the + parameter types listed by a supported address types parameter or a + missing mandatory parameter cause -- take a member or an + :class:`int` code, resolved with the enumeration's ``get``, and a + code no member holds is written as given; a name, a :obj:`bool` for a + field wider than one bit, or a code the field cannot carry raises + :exc:`~pcapkit.utilities.exceptions.ProtocolError` (:issue:`2594`). """ @@ -694,7 +704,7 @@ def make(self, dstport: 'Enum_AppType | int' = 0, vtag: 'int' = 0, chksum: 'Optional[bytes]' = None, - chunks: 'Optional[list[Schema_Chunk | tuple[Enum_Chunk, dict[str, Any]] | bytes] | Chunks]' = None, # pylint: disable=line-too-long + chunks: 'Optional[list[Schema_Chunk | tuple[Enum_Chunk | int, dict[str, Any]] | bytes] | Chunks]' = None, # pylint: disable=line-too-long trailer: 'bytes' = b'', **kwargs: 'Any') -> 'Schema_SCTP': """Make (construct) packet data. @@ -717,7 +727,9 @@ def make(self, Raises: ProtocolError: If ``srcport`` or ``dstport`` is a :obj:`bool` (:issue:`2287`), ``vtag`` does not fit 32 bits, ``chksum`` is - not four octets, or a chunk maker rejects its chunk. + not four octets, a chunk's type is a name, a :obj:`bool`, or a + code the 8-bit type field cannot carry (:issue:`2594`), or a + chunk maker rejects its chunk. Note: There is no ``payload`` argument: SCTP carries its user data in the @@ -1024,7 +1036,7 @@ def _read_sctp_chunks(self) -> 'Chunks': chunks.add(code, _keep_padding(schema, meth(schema, chunks=chunks))) return chunks - def _make_sctp_chunks(self, chunks: 'list[Schema_Chunk | tuple[Enum_Chunk, dict[str, Any]] | bytes] | Chunks') -> 'list[Schema_Chunk | bytes]': # pylint: disable=line-too-long + def _make_sctp_chunks(self, chunks: 'list[Schema_Chunk | tuple[Enum_Chunk | int, dict[str, Any]] | bytes] | Chunks') -> 'list[Schema_Chunk | bytes]': # pylint: disable=line-too-long """Make chunks for SCTP. Args: @@ -1054,11 +1066,13 @@ def _make_sctp_chunks(self, chunks: 'list[Schema_Chunk | tuple[Enum_Chunk, dict[ elif isinstance(schema, Schema): chunks_list.append(schema) else: - code, args = cast('tuple[Enum_Chunk, dict[str, Any]]', schema) + kind, args = cast('tuple[Enum_Chunk | int, dict[str, Any]]', schema) + code = self._make_index(kind, Enum_Chunk, bits=8, name='chunk type') chunks_list.append(self._make_sctp_chunk(code, None, **args)) return _last_item_only(chunks_list) - for code, chunk in chunks.items(multi=True): + for key, chunk in chunks.items(multi=True): + code = self._make_index(key, Enum_Chunk, bits=8, name='chunk type') chunks_list.append(self._make_sctp_chunk(code, chunk)) return _last_item_only(chunks_list) @@ -1146,7 +1160,7 @@ def _read_sctp_parameters(self, schemas: 'list[Schema_Parameter]') -> 'Parameter parameters.add(code, _keep_padding(schema, meth(schema, parameters=parameters))) return parameters - def _make_sctp_parameters(self, parameters: 'list[Schema_Parameter | tuple[Enum_Parameter, dict[str, Any]] | bytes] | Parameters') -> 'list[Schema_Parameter | bytes]': # pylint: disable=line-too-long + def _make_sctp_parameters(self, parameters: 'list[Schema_Parameter | tuple[Enum_Parameter | int, dict[str, Any]] | bytes] | Parameters') -> 'list[Schema_Parameter | bytes]': # pylint: disable=line-too-long """Make chunk parameters for SCTP. Args: @@ -1163,11 +1177,13 @@ def _make_sctp_parameters(self, parameters: 'list[Schema_Parameter | tuple[Enum_ if isinstance(schema, (bytes, Schema)): parameters_list.append(schema) else: - code, args = cast('tuple[Enum_Parameter, dict[str, Any]]', schema) + kind, args = cast('tuple[Enum_Parameter | int, dict[str, Any]]', schema) + code = self._make_index(kind, Enum_Parameter, bits=16, name='parameter type') parameters_list.append(self._make_sctp_parameter(code, None, **args)) return _last_item_only(parameters_list) - for code, parameter in parameters.items(multi=True): + for key, parameter in parameters.items(multi=True): + code = self._make_index(key, Enum_Parameter, bits=16, name='parameter type') parameters_list.append(self._make_sctp_parameter(code, parameter)) return _last_item_only(parameters_list) @@ -1218,7 +1234,7 @@ def _read_sctp_causes(self, schemas: 'list[Schema_ErrorCause]') -> 'Causes': causes.add(code, _keep_padding(schema, meth(schema, causes=causes))) return causes - def _make_sctp_causes(self, causes: 'list[Schema_ErrorCause | tuple[Enum_CauseCode, dict[str, Any]] | bytes] | Causes') -> 'list[Schema_ErrorCause | bytes]': # pylint: disable=line-too-long + def _make_sctp_causes(self, causes: 'list[Schema_ErrorCause | tuple[Enum_CauseCode | int, dict[str, Any]] | bytes] | Causes') -> 'list[Schema_ErrorCause | bytes]': # pylint: disable=line-too-long """Make error causes for SCTP. Args: @@ -1235,11 +1251,13 @@ def _make_sctp_causes(self, causes: 'list[Schema_ErrorCause | tuple[Enum_CauseCo if isinstance(schema, (bytes, Schema)): causes_list.append(schema) else: - code, args = cast('tuple[Enum_CauseCode, dict[str, Any]]', schema) + kind, args = cast('tuple[Enum_CauseCode | int, dict[str, Any]]', schema) + code = self._make_index(kind, Enum_CauseCode, bits=16, name='cause code') causes_list.append(self._make_sctp_cause(code, None, **args)) return _last_item_only(causes_list) - for code, cause in causes.items(multi=True): + for key, cause in causes.items(multi=True): + code = self._make_index(key, Enum_CauseCode, bits=16, name='cause code') causes_list.append(self._make_sctp_cause(code, cause)) return _last_item_only(causes_list) @@ -1302,27 +1320,22 @@ def _make_param_types(self, types: 'Optional[list[Enum_Parameter | int]]') -> 'l Args: types: parameter types, each given as a - :class:`~pcapkit.const.sctp.parameter.Parameter` or an - :obj:`int` + :class:`~pcapkit.const.sctp.parameter.Parameter` member or an + :obj:`int` code Returns: - The :class:`~pcapkit.const.sctp.parameter.Parameter` for each - item. + The :class:`~pcapkit.const.sctp.parameter.Parameter` member each + item resolves to with ``get``, or the code itself where no member + holds it. Raises: - ProtocolError: If an item is a :obj:`bool`, or is neither a - :class:`~pcapkit.const.sctp.parameter.Parameter` nor an - :obj:`int` (:issue:`2288`). + ProtocolError: If an item is a :obj:`bool` (:issue:`2288`), a name, + any other non-:obj:`int`, or a code the 16-bit parameter type + field cannot carry (:issue:`2594`). """ - types_value = [] # type: list[Enum_Parameter] - for item in types or []: - # NOTE: ``bool`` is an ``int`` subclass, so ``Parameter.get`` - # would look ``True`` up as parameter type 1 (#2288). - if isinstance(item, bool) or not isinstance(item, int): - raise ProtocolError(f'{self.alias}: invalid parameter type: {item!r}') - types_value.append(Enum_Parameter.get(item)) - return types_value + return [self._make_index(item, Enum_Parameter, bits=16, name='parameter type') + for item in types or []] def _read_chunk_donone(self, schema: 'Schema_UnknownChunk', *, chunks: 'Chunks') -> 'Data_UnknownChunk': # pylint: disable=unused-argument """Read SCTP chunk of an unsupported type. @@ -1964,7 +1977,8 @@ def _make_chunk_data(self, code: 'Enum_Chunk', chunk: 'Optional[Data_DATAChunk]' tsn: transmission sequence number stream_id: stream identifier stream_seq: stream sequence number - ppid: payload protocol identifier + ppid: payload protocol identifier, as a member or an :obj:`int` + code data: user data **kwargs: arbitrary keyword arguments @@ -1973,8 +1987,9 @@ def _make_chunk_data(self, code: 'Enum_Chunk', chunk: 'Optional[Data_DATAChunk]' Raises: ProtocolError: If ``data`` is empty, since :rfc:`9260#section-3.3.1` - requires at least one byte of user data, or a field does not - fit its width. + requires at least one byte of user data, a field does not fit + its width, or ``ppid`` is a name, a :obj:`bool` or a code the + field cannot carry. """ if chunk is not None: @@ -2006,7 +2021,8 @@ def _make_chunk_data(self, code: 'Enum_Chunk', chunk: 'Optional[Data_DATAChunk]' tsn=self._make_uint(tsn, bits=32, name=f'[Chunk {code}] tsn'), stream_id=self._make_uint(stream_id, bits=16, name=f'[Chunk {code}] stream_id'), stream_seq=self._make_uint(stream_seq, bits=16, name=f'[Chunk {code}] stream_seq'), - ppid=self._make_uint(ppid, bits=32, name=f'[Chunk {code}] ppid'), + ppid=self._make_index(ppid, Enum_PayloadProtocolIdentifier, bits=32, + name=f'[Chunk {code}] ppid'), data=data, ) @@ -2017,7 +2033,7 @@ def _make_chunk_init(self, code: 'Enum_Chunk', chunk: 'Optional[Data_INITChunk]' outbound_streams: 'int' = 1, inbound_streams: 'int' = 1, init_tsn: 'int' = 0, - parameters: 'Optional[list[Schema_Parameter | tuple[Enum_Parameter, dict[str, Any]] | bytes] | Parameters]' = None, # pylint: disable=line-too-long + parameters: 'Optional[list[Schema_Parameter | tuple[Enum_Parameter | int, dict[str, Any]] | bytes] | Parameters]' = None, # pylint: disable=line-too-long **kwargs: 'Any') -> 'Schema_INITChunk': """Make SCTP INIT chunk. @@ -2082,7 +2098,7 @@ def _make_chunk_init_ack(self, code: 'Enum_Chunk', chunk: 'Optional[Data_INITACK outbound_streams: 'int' = 1, inbound_streams: 'int' = 1, init_tsn: 'int' = 0, - parameters: 'Optional[list[Schema_Parameter | tuple[Enum_Parameter, dict[str, Any]] | bytes] | Parameters]' = None, # pylint: disable=line-too-long + parameters: 'Optional[list[Schema_Parameter | tuple[Enum_Parameter | int, dict[str, Any]] | bytes] | Parameters]' = None, # pylint: disable=line-too-long **kwargs: 'Any') -> 'Schema_INITACKChunk': """Make SCTP INIT ACK chunk. @@ -2207,7 +2223,7 @@ def _make_chunk_sack(self, code: 'Enum_Chunk', chunk: 'Optional[Data_SACKChunk]' def _make_chunk_heartbeat(self, code: 'Enum_Chunk', chunk: 'Optional[Data_HeartbeatChunk]' = None, *, flags: 'bytes' = b'\x00', - parameters: 'Optional[list[Schema_Parameter | tuple[Enum_Parameter, dict[str, Any]] | bytes] | Parameters]' = None, # pylint: disable=line-too-long + parameters: 'Optional[list[Schema_Parameter | tuple[Enum_Parameter | int, dict[str, Any]] | bytes] | Parameters]' = None, # pylint: disable=line-too-long **kwargs: 'Any') -> 'Schema_HeartbeatChunk': """Make SCTP HEARTBEAT chunk. @@ -2247,7 +2263,7 @@ def _make_chunk_heartbeat(self, code: 'Enum_Chunk', chunk: 'Optional[Data_Heartb def _make_chunk_heartbeat_ack(self, code: 'Enum_Chunk', chunk: 'Optional[Data_HeartbeatACKChunk]' = None, *, flags: 'bytes' = b'\x00', - parameters: 'Optional[list[Schema_Parameter | tuple[Enum_Parameter, dict[str, Any]] | bytes] | Parameters]' = None, # pylint: disable=line-too-long + parameters: 'Optional[list[Schema_Parameter | tuple[Enum_Parameter | int, dict[str, Any]] | bytes] | Parameters]' = None, # pylint: disable=line-too-long **kwargs: 'Any') -> 'Schema_HeartbeatACKChunk': """Make SCTP HEARTBEAT ACK chunk. @@ -2288,7 +2304,7 @@ def _make_chunk_heartbeat_ack(self, code: 'Enum_Chunk', chunk: 'Optional[Data_He def _make_chunk_abort(self, code: 'Enum_Chunk', chunk: 'Optional[Data_AbortChunk]' = None, *, reserved: 'int' = 0, T: 'bool' = False, - error: 'Optional[list[Schema_ErrorCause | tuple[Enum_CauseCode, dict[str, Any]] | bytes] | Causes]' = None, # pylint: disable=line-too-long + error: 'Optional[list[Schema_ErrorCause | tuple[Enum_CauseCode | int, dict[str, Any]] | bytes] | Causes]' = None, # pylint: disable=line-too-long **kwargs: 'Any') -> 'Schema_AbortChunk': """Make SCTP ABORT chunk. @@ -2394,7 +2410,7 @@ def _make_chunk_shutdown_ack(self, code: 'Enum_Chunk', chunk: 'Optional[Data_Shu def _make_chunk_error(self, code: 'Enum_Chunk', chunk: 'Optional[Data_ErrorChunk]' = None, *, flags: 'bytes' = b'\x00', - error: 'Optional[list[Schema_ErrorCause | tuple[Enum_CauseCode, dict[str, Any]] | bytes] | Causes]' = None, # pylint: disable=line-too-long + error: 'Optional[list[Schema_ErrorCause | tuple[Enum_CauseCode | int, dict[str, Any]] | bytes] | Causes]' = None, # pylint: disable=line-too-long **kwargs: 'Any') -> 'Schema_ErrorChunk': """Make SCTP ERROR chunk. @@ -3068,7 +3084,8 @@ def _make_param_addrtypes(self, code: 'Enum_Parameter', param: 'Optional[Data_Su Args: code: parameter type param: parameter data - types: supported address types, given as address parameter types + types: supported address types, given as address parameter types, + each a member or an :obj:`int` code **kwargs: arbitrary keyword arguments Returns: @@ -3634,7 +3651,8 @@ def _make_cause_missing_param(self, code: 'Enum_CauseCode', cause: 'Optional[Dat Args: code: error cause code cause: error cause data - types: missing parameter types + types: missing parameter types, each a member or an :obj:`int` + code **kwargs: arbitrary keyword arguments Returns: diff --git a/pcapkit/protocols/transport/tcp.py b/pcapkit/protocols/transport/tcp.py index d89bde9fe1..d7fa2b3b04 100644 --- a/pcapkit/protocols/transport/tcp.py +++ b/pcapkit/protocols/transport/tcp.py @@ -142,11 +142,9 @@ if TYPE_CHECKING: from datetime import timedelta - from enum import IntEnum as StdlibEnum from ipaddress import IPv4Address, IPv6Address from typing import Any, Callable, ClassVar, DefaultDict, Optional, Type - from aenum import IntEnum as AenumEnum from mypy_extensions import DefaultArg, KwArg, NamedArg from typing_extensions import Literal @@ -353,6 +351,25 @@ class TCP(Transport[Data_TCP, Schema_TCP], :class:`~datetime.timedelta` raises the same, rather than an :exc:`AttributeError`. The Quick-Start TTL Diff wraps, as :rfc:`4782` defines it modulo 256 (:issue:`2614`). + * Each enumeration field of the option makers, the alternate checksum + ``algorithm`` and the Multipath TCP ``subtype``, and the kind of each + option given to :meth:`make`, takes a member or an :class:`int` code, + resolved with the enumeration's ``get``; a name, a :obj:`bool`, or a + code the field cannot carry raises + :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than + resolving against an enumeration inferred from the caller's frame, + writing a different value or raising another exception + (:issue:`2594`). + + .. versionremoved:: 1.6.0 + + The ``algorithm_default``, ``algorithm_namespace`` and + ``algorithm_reversed`` arguments of the alternate checksum request + option maker, and the ``subtype_default``, ``subtype_namespace`` and + ``subtype_reversed`` arguments of the Multipath TCP option maker; + resolve a name with + :meth:`~pcapkit.protocols.protocol.Protocol.resolve_index` first + (:issue:`2594`). """ @@ -622,7 +639,7 @@ def make(self, window: 'int' = 65535, # reasonable default value checksum: 'Optional[bytes]' = None, urgent: 'int' = 0, - options: 'Optional[list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes] | Option]' = None, # pylint: disable=line-too-long + options: 'Optional[list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes] | Option]' = None, # pylint: disable=line-too-long payload: 'bytes | ProtocolBase | Schema' = b'', padding: 'bytes' = b'', reserved: 'int' = 0, @@ -921,7 +938,8 @@ def _read_tcp_options(self, size: 'int') -> 'Option': raise ProtocolError('TCP: invalid format') return options - def _make_tcp_options(self, options: 'list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes] | Option', + def _make_tcp_options(self, + options: 'list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes] | Option', padding: 'bytes' = b'') -> 'tuple[list[Schema_Option | bytes], int]': """Make options for TCP. @@ -933,12 +951,12 @@ def _make_tcp_options(self, options: 'list[Schema_Option | tuple[Enum_Option, di Tuple of options and total length of options. Raises: - ProtocolError: If an option maker rejects its option, e.g. a user - timeout too large for 15 bits of minutes, or an MPTCP option - whose fields, or the segment's flags, it cannot encode. - ProtocolNotImplemented: If an option argument given by name, an - MPTCP ``subtype`` or an alternate checksum ``algorithm``, is - not in its namespace and has no default. + ProtocolError: If an option's kind is a name, a :obj:`bool`, or a + code the 8-bit kind octet cannot carry (:issue:`2594`), or if + an option maker rejects its option, e.g. a user timeout too + large for 15 bits of minutes, an MPTCP ``subtype`` or an + alternate checksum ``algorithm`` given by name, or an MPTCP + option whose fields, or the segment's flags, it cannot encode. FieldValueError: If an ADD_ADDR ``addr`` is a :obj:`bool` or not a valid IP address. @@ -966,7 +984,8 @@ def _make_tcp_options(self, options: 'list[Schema_Option | tuple[Enum_Option, di data = schema data_len = len(schema.pack()) else: - code, args = cast('tuple[Enum_Option, dict[str, Any]]', schema) + kind, args = cast('tuple[Enum_Option | int, dict[str, Any]]', schema) + code = self._make_index(kind, Enum_Option, bits=8, name='option kind') name = self._lookup_registry(self.__option__, code) if isinstance(name, str): meth_name = f'_make_mode_{name}' @@ -981,7 +1000,8 @@ def _make_tcp_options(self, options: 'list[Schema_Option | tuple[Enum_Option, di options_list.append(data) total_length += data_len else: - for code, option in options.items(multi=True): + for key, option in options.items(multi=True): + code = self._make_index(key, Enum_Option, bits=8, name='option kind') name = self._lookup_registry(self.__option__, code) if isinstance(name, str): meth_name = f'_make_mode_{name}' @@ -2728,10 +2748,7 @@ def _make_mode_ccecho(self, code: 'Enum_Option', opt: 'Optional[Data_CCEcho]' = ) def _make_mode_chkreq(self, code: 'Enum_Option', opt: 'Optional[Data_AlternateChecksumRequest]' = None, *, - algorithm: 'Enum_Checksum | StdlibEnum | AenumEnum | int | str' = Enum_Checksum.TCP_checksum, - algorithm_default: 'Optional[int]' = None, - algorithm_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - algorithm_reversed: 'bool' = False, + algorithm: 'Enum_Checksum | int' = Enum_Checksum.TCP_checksum, **kwargs: 'Any') -> 'Schema_AlternateChecksumRequest': """Make TCP alternate checksum request option. @@ -2739,28 +2756,23 @@ def _make_mode_chkreq(self, code: 'Enum_Option', opt: 'Optional[Data_AlternateCh code: option code opt: option data algorithm: checksum algorithm - algorithm_default: default value for checksum algorithm - algorithm_namespace: namespace for checksum algorithm - algorithm_reversed: reversed flag for checksum algorithm **kwargs: arbitrary keyword arguments Returns: Constructed option schema. Raises: - ProtocolError: If ``algorithm`` does not fit 8 bits. + ProtocolError: If ``algorithm`` is a name, a :obj:`bool`, or a + member or code that does not fit 8 bits. """ if opt is not None: - algorithm_val = opt.chksum - else: - algorithm_val = self._make_index(algorithm, algorithm_default, namespace=algorithm_namespace, # type: ignore[assignment] - reversed=algorithm_reversed, pack=False) + algorithm = opt.chksum return Schema_AlternateChecksumRequest( kind=code, length=3, - algorithm=cast('Enum_Checksum', self._make_uint(algorithm_val, bits=8, name=f'[OptNo {code}] algorithm')), + algorithm=self._make_index(algorithm, Enum_Checksum, bits=8, name=f'[OptNo {code}] algorithm'), ) def _make_mode_chksum(self, code: 'Enum_Option', opt: 'Optional[Data_AlternateChecksumData]' = None, *, @@ -2968,10 +2980,7 @@ def _make_mode_ao(self, code: 'Enum_Option', opt: 'Optional[Data_Authentication] ) def _make_mode_mp(self, code: 'Enum_Option', opt: 'Optional[Data_MPTCP]' = None, *, - subtype: 'Enum_MPTCPOption | StdlibEnum | AenumEnum | int | str' = Enum_MPTCPOption.MP_CAPABLE, - subtype_default: 'Optional[int]' = None, - subtype_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long - subtype_reversed: 'bool' = False, + subtype: 'Enum_MPTCPOption | int' = Enum_MPTCPOption.MP_CAPABLE, **kwargs: 'Any') -> 'Schema_MPTCP': """Make multipath TCP option. @@ -2979,21 +2988,19 @@ def _make_mode_mp(self, code: 'Enum_Option', opt: 'Optional[Data_MPTCP]' = None, code: option code opt: option data subtype: MPTCP subtype - subtype_default: default value for MPTCP subtype - subtype_namespace: namespace for MPTCP subtype - subtype_reversed: reversed flag for MPTCP subtype **kwargs: arbitrary keyword arguments Returns: Constructed option schema. + Raises: + ProtocolError: If ``subtype`` is a name, a :obj:`bool`, or a member + or code that does not fit 4 bits. + """ if opt is not None: - subtype_val = opt.subtype - else: - subtype_val = self._make_index(subtype, subtype_default, namespace=subtype_namespace, # type: ignore[assignment] - reversed=subtype_reversed, pack=False) - subtype_val = Enum_MPTCPOption.get(subtype_val) + subtype = opt.subtype + subtype_val = self._make_index(subtype, Enum_MPTCPOption, bits=4, name=f'[OptNo {code}] subtype') name = self._lookup_registry(self.__mp_option__, subtype_val) if isinstance(name, str): @@ -3054,7 +3061,7 @@ def _make_mptcp_unknown(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_M kind=cast('Enum_Option', Enum_Option.Multipath_TCP), length=3 + len(data[1:]), test={ - 'subtype': subtype.value, + 'subtype': int(subtype), 'data': self._make_uint(data[0], bits=4, name=f'[OptNo {Enum_Option.Multipath_TCP}] ' f'subtype {subtype} first data octet') if data else 0, }, diff --git a/pcapkit/protocols/transport/transport.py b/pcapkit/protocols/transport/transport.py index 785ddac543..3ba139c7f6 100644 --- a/pcapkit/protocols/transport/transport.py +++ b/pcapkit/protocols/transport/transport.py @@ -179,7 +179,7 @@ def analyze(cls, ports: 'tuple[Enum_AppType, Enum_AppType]', payload: 'bytes', * ``.port`` unconditionally would have made the :obj:`int` case an :exc:`AttributeError`, and unwrapping unconditionally would have changed what anything *else* does here: :func:`int` of :obj:`None` - raises where the lookup used to fall back to + raises where the lookup falls back to :class:`~pcapkit.protocols.misc.raw.Raw`, and :func:`int` of ``'80'`` would dispatch a :class:`str` nobody registered. @@ -324,7 +324,9 @@ def _make_port(cls, port: 'Enum_AppType | int', The :class:`~pcapkit.const.reg.apptype.apptype.AppType` for ``port``. Raises: - ProtocolError: If ``port`` is a :obj:`bool` (:issue:`2287`). + ProtocolError: If ``port`` is a :obj:`bool` (:issue:`2287`), or is + neither an :class:`int` that fits in 16 bits nor an + :class:`~pcapkit.const.reg.apptype.apptype.AppType`. Important: :meth:`self.make ` accepts @@ -337,14 +339,24 @@ def _make_port(cls, port: 'Enum_AppType | int', :class:`~pcapkit.const.reg.apptype.apptype.AppType` as a parsed one, so reading ``.port`` off it works on both paths. + .. versionchanged:: 1.6.0 + + A port past 16 bits, a negative one, or one that is not an + :class:`int` raises :exc:`~pcapkit.utilities.exceptions.ProtocolError`, + rather than a bare :exc:`~pcapkit.utilities.exceptions.EnumValueError` + (:issue:`2594`). + """ if isinstance(port, Enum_AppType): return port # NOTE: ``bool`` is an ``int`` subclass, so ``AppType.get`` would look # ``True``/``False`` up as port 1/0 rather than refuse them (#2287). - if isinstance(port, bool): + if isinstance(port, bool) or not isinstance(port, int): raise ProtocolError(f'{cls.__name__}: invalid port: {port!r}') - return Enum_AppType.get(port, proto=proto) + try: + return Enum_AppType.get(port, proto=proto) + except ValueError as error: # the registry's own range, quiet as get raises it + raise ProtocolError(f'{cls.__name__}: invalid port: {port!r}') from error def _decode_next_layer(self, dict_: '_PT', ports: 'tuple[int, int]', length: 'Optional[int]' = None, *, # type: ignore[override] packet: 'Optional[dict[str, Any]]' = None) -> '_PT': # pylint: disable=arguments-renamed diff --git a/tests/dumpkit/test_pcapio_linktype_name_2594_unit.py b/tests/dumpkit/test_pcapio_linktype_name_2594_unit.py new file mode 100644 index 0000000000..7f09338417 --- /dev/null +++ b/tests/dumpkit/test_pcapio_linktype_name_2594_unit.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +"""The PCAP dumper takes its link type as a name, a member or a code (GitHub issue #2594). + +``Header.make`` takes a member or an :class:`int` code for ``network`` and refuses a +name (#2594). :class:`~pcapkit.dumpkit.pcap.PCAPIO`'s ``protocol`` takes a name as +well, which ``PCAPIO._dump_header`` resolves with ``Header.resolve_index`` before it +reaches ``Header.make``: a known name writes its link type, and an unknown one raises +:exc:`~pcapkit.utilities.exceptions.ProtocolNotImplemented`, not the +:exc:`~pcapkit.utilities.exceptions.ProtocolError` ``Header.make`` raises for a name. + +Every case writes its own header into a temporary directory and reads no capture. +Classes are imported inside each test, after +:func:`~tests._support.reimport_once_per_class`. + +""" + +import os +import struct +import tempfile +import unittest +from typing import TYPE_CHECKING + +from tests._support import reimport_once_per_class + +if TYPE_CHECKING: + from typing import Any + + +class TestPCAPIOLinkType(unittest.TestCase): + """``PCAPIO(path, protocol=...)`` writes the link type it names.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + self.tmpdir = tempfile.TemporaryDirectory(prefix='pcapkit-pcapio-linktype-') + self.addCleanup(self.tmpdir.cleanup) + + def _header(self, protocol: 'Any') -> 'bytes': + from pcapkit.dumpkit.pcap import PCAPIO + + path = os.path.join(self.tmpdir.name, f'{len(os.listdir(self.tmpdir.name))}.pcap') + PCAPIO(path, protocol=protocol, byteorder='little', nanosecond=False) + with open(path, 'rb') as file: + return file.read() + + def test_a_name_writes_its_link_type(self) -> None: + from pcapkit.const.reg.linktype import LinkType + + header = self._header('ETHERNET') + self.assertEqual(header, self._header(LinkType.ETHERNET)) + self.assertEqual(struct.unpack(' None: + from pcapkit.const.reg.linktype import LinkType + + self.assertEqual(self._header(1), self._header(LinkType.ETHERNET)) + # a code no member holds is written as given + self.assertEqual(struct.unpack(' None: + from pcapkit.utilities.exceptions import ProtocolError, ProtocolNotImplemented + + with self.assertRaises(ProtocolNotImplemented) as context: + self._header('NO_SUCH_LINKTYPE') + self.assertNotIsInstance(context.exception, ProtocolError) + + def test_a_code_the_field_cannot_carry_is_refused(self) -> None: + from pcapkit.utilities.exceptions import ProtocolError + + for value in (True, -1, 1 << 32): + with self.subTest(value=value): + with self.assertRaises(ProtocolError): + self._header(value) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/project/test_make_keyword_groups_2594_unit.py b/tests/project/test_make_keyword_groups_2594_unit.py new file mode 100644 index 0000000000..5384edd795 --- /dev/null +++ b/tests/project/test_make_keyword_groups_2594_unit.py @@ -0,0 +1,188 @@ +# -*- coding: utf-8 -*- +"""No ``make()`` takes an ``X_default``/``X_namespace``/``X_reversed`` keyword, and +``_make_index`` inspects no frame (GitHub issue #2594). + +The owner ruled, in review of #2593 (which closes #2489), that a ``make()`` field +holding an enumeration takes the member or an :class:`int`, resolved with the +enumeration's ``get``, and that no string is resolved inside ``make()``; #2594 holds +every maker to it, with no ``X_default``, ``X_namespace`` or ``X_reversed`` keyword +beside such a field and no name resolved against an enumeration read off the calling +method's frame. + +This module holds the tree to that: + +* :class:`TestNoKeywordGroups` parses every protocol module and fails on a + ``make()`` or ``_make_*`` method taking a parameter named ``X_default``, + ``X_namespace`` or ``X_reversed``, and on a ``_make_index`` call that passes + ``default``, ``reversed``, ``pack`` or ``size``, or leaves out the field's ``bits`` + and ``name``; +* :class:`TestMakeIndexInspectsNoFrame` parses ``_make_index`` and ``resolve_index`` + and fails on any reference to a frame, a call stack or a caller's source; +* :class:`TestInferenceHelpersAreGone` fails on ``_make_index_calls`` or + ``_make_index_namespace`` being defined. + +:data:`PENDING` holds the methods allowed to take a group, each with what removes it. +It is checked in both directions, as ``test-coverage.rst`` asks of such tables: an +entry whose method takes no group, or does not exist, fails too, so the +change that migrates it deletes its entry. + +""" +from __future__ import annotations + +import ast +import importlib +import re +import unittest +from typing import TYPE_CHECKING + +from tests._support import reimport_once_per_class +from tests._tiers import ROOT + +if TYPE_CHECKING: + from typing import Iterator + +#: Test tier (#1958): parses every protocol module for a question about the tree, +#: which does not differ by interpreter. +__tier__ = 'LATEST' + +#: The protocol package the audit walks. +PROTOCOLS = ROOT / 'pcapkit' / 'protocols' + +#: Directory names the audit does not enter: placeholder sources and caches. +SKIPPED_DIRS = frozenset({'NotImplemented', '__pycache__'}) + +#: A parameter of an ``X_default``/``X_namespace``/``X_reversed`` group. +GROUP_PARAMETER = re.compile(r'^(?P\w+)_(?:default|namespace|reversed)$') + +#: Keywords no ``_make_index`` call passes. +OLD_KEYWORDS = frozenset({'default', 'reversed', 'pack', 'size', 'signed', 'lilendian'}) + +#: Keywords every ``_make_index`` call passes. +REQUIRED_KEYWORDS = frozenset({'bits', 'name'}) + +#: Names that reach a frame, a call stack or a caller's source. +FRAME_NAMES = frozenset({ + 'inspect', 'sys', 'traceback', + 'currentframe', '_getframe', 'stack', 'getouterframes', 'getinnerframes', + 'getframeinfo', 'getsource', 'getsourcelines', 'signature', + 'f_back', 'f_locals', 'f_globals', 'f_code', 'f_lineno', 'tb_frame', +}) + +#: Methods allowed to take a group, as ``(module path, class, method)``: what removes +#: each. +PENDING = {} # type: dict[tuple[str, str, str], str] + + +def _modules() -> 'Iterator[tuple[str, ast.Module]]': + """Every protocol module, as its path from the root and its parsed source.""" + for path in sorted(PROTOCOLS.rglob('*.py')): + if SKIPPED_DIRS.intersection(path.relative_to(PROTOCOLS).parts): + continue + yield path.relative_to(ROOT).as_posix(), ast.parse(path.read_text(encoding='utf-8')) + + +def _makers() -> 'Iterator[tuple[tuple[str, str, str], ast.FunctionDef]]': + """Every ``make()`` and ``_make_*`` method of a class, keyed by where it is.""" + for path, tree in _modules(): + for klass in ast.walk(tree): + if not isinstance(klass, ast.ClassDef): + continue + for node in klass.body: + if isinstance(node, ast.FunctionDef) and (node.name == 'make' or node.name.startswith('_make_')): + yield (path, klass.name, node.name), node + + +def _groups(node: 'ast.FunctionDef') -> 'list[str]': + """The group parameters ``node`` takes.""" + params = node.args.posonlyargs + node.args.args + node.args.kwonlyargs + return sorted(param.arg for param in params if GROUP_PARAMETER.match(param.arg)) + + +class TestNoKeywordGroups(unittest.TestCase): + """No ``make()`` takes the ``X_default``/``X_namespace``/``X_reversed`` group.""" + + def test_no_maker_takes_a_group(self) -> None: + found = {key: _groups(node) for key, node in _makers()} + offenders = sorted(f'{path}: {klass}.{meth}({", ".join(params)})' + for (path, klass, meth), params in found.items() + if params and (path, klass, meth) not in PENDING) + self.assertEqual(offenders, [], 'a make() takes a keyword group; an enumeration field takes ' + 'the member or an int, resolved with _make_index (#2594)') + + def test_pending_entries_still_take_a_group(self) -> None: + found = {key: _groups(node) for key, node in _makers()} + stale = sorted(f'{path}: {klass}.{meth}' for path, klass, meth in PENDING + if not found.get((path, klass, meth))) + self.assertEqual(stale, [], 'a PENDING method takes no group; delete its entry') + + def test_make_index_calls_take_the_field_width(self) -> None: + offenders = [] + for path, tree in _modules(): + for klass in ast.walk(tree): + if not isinstance(klass, ast.ClassDef): + continue + for meth in klass.body: + if not isinstance(meth, ast.FunctionDef) or meth.name == '_make_index': + continue + if (path, klass.name, meth.name) in PENDING: + continue + for node in ast.walk(meth): + if not (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) + and node.func.attr == '_make_index'): + continue + keywords = {keyword.arg for keyword in node.keywords} + if (len(node.args) != 2 or keywords & OLD_KEYWORDS + or not REQUIRED_KEYWORDS <= keywords): + offenders.append(f'{path}: {klass.name}.{meth.name}') + self.assertEqual(sorted(offenders), [], + '_make_index takes (value, enumeration, bits=..., name=...) (#2594)') + + +class TestMakeIndexInspectsNoFrame(unittest.TestCase): + """The resolver reads its arguments and nothing of its caller.""" + + @staticmethod + def _defs(prefix: 'str') -> 'list[ast.FunctionDef]': + """Every definition in :mod:`pcapkit.protocols.protocol` named ``prefix`` or + ``prefix_*``, at module level or on ``ProtocolBase``, overloads included.""" + tree = ast.parse((PROTOCOLS / 'protocol.py').read_text(encoding='utf-8')) + scopes = [tree.body] + [klass.body for klass in tree.body + if isinstance(klass, ast.ClassDef) and klass.name == 'ProtocolBase'] + return [node for body in scopes for node in body if isinstance(node, ast.FunctionDef) + and (node.name == prefix or node.name.startswith(f'{prefix}_'))] + + def test_resolvers_reach_no_frame(self) -> None: + for prefix in ('_make_index', 'resolve_index'): + with self.subTest(method=prefix): + defs = self._defs(prefix) + self.assertTrue(defs, f'ProtocolBase.{prefix} is not defined') + used = set() # type: set[str] + for node in defs: + body = ast.Module(body=node.body, type_ignores=[]) + used |= {item.id for item in ast.walk(body) if isinstance(item, ast.Name)} + used |= {item.attr for item in ast.walk(body) if isinstance(item, ast.Attribute)} + self.assertEqual(sorted(used & FRAME_NAMES), [], + f'{prefix} reaches a frame or a caller; it reads only its arguments (#2594)') + + def test_make_index_takes_no_old_keyword(self) -> None: + # the last definition is the implementation, after any overloads + node = [node for node in self._defs('_make_index') if node.name == '_make_index'][-1] + params = {param.arg for param in node.args.args + node.args.kwonlyargs} + self.assertEqual(sorted(params & OLD_KEYWORDS), []) + self.assertLessEqual(REQUIRED_KEYWORDS, params) + + +class TestInferenceHelpersAreGone(unittest.TestCase): + """The helpers the frame inspection used are not defined.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def test_no_inference_helpers(self) -> None: + module = importlib.import_module('pcapkit.protocols.protocol') + self.assertFalse(hasattr(module, '_make_index_calls')) + self.assertFalse(hasattr(module.ProtocolBase, '_make_index_namespace')) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/project/test_rfc_constants_one_home_1739_unit.py b/tests/project/test_rfc_constants_one_home_1739_unit.py index da8a13873a..b6807a6968 100644 --- a/tests/project/test_rfc_constants_one_home_1739_unit.py +++ b/tests/project/test_rfc_constants_one_home_1739_unit.py @@ -643,8 +643,9 @@ def _context(text: 'str', operand: 'ast.Constant', parents: 'dict[int, ast.AST]' return _segment(text, node) -#: Value helpers and the keyword each takes a field width by, in bits or octets (#2614). -_WIDTHS = {'_make_uint': 'bits', '_make_sint': 'bits', '_make_pack': 'size', +#: Value helpers and the keyword each takes a field width by, in bits or octets (#2614, +#: #2594). +_WIDTHS = {'_make_uint': 'bits', '_make_sint': 'bits', '_make_index': 'bits', '_make_pack': 'size', '_read_uint': 'size', '_make_octets': 'size'} @@ -838,8 +839,8 @@ def test_sweep(self) -> 'None': else: offenders.append(f'{rel}:{lineno}: {context} (a copy of a homed constant?)') self.assertEqual(offenders, []) - # an entry that no longer matches anything is stale, and would hide a - # copy that later took its place + # an entry that matches nothing is stale, and would hide a copy that + # took its place self.assertEqual(sorted(set(NOT_A_COPY) - seen_copy), []) listed = {(rel, value) for rel, (values, _) in UNRELATED.items() for value in values} self.assertEqual(sorted(listed - seen_unrelated), []) diff --git a/tests/protocols/application/test_application_make_codes_2594_unit.py b/tests/protocols/application/test_application_make_codes_2594_unit.py new file mode 100644 index 0000000000..3c352d4170 --- /dev/null +++ b/tests/protocols/application/test_application_make_codes_2594_unit.py @@ -0,0 +1,217 @@ +# -*- coding: utf-8 -*- +"""An application-layer enumeration field takes a member or a code, never a name. + +GitHub issue #2594, ruled in review of #2593 (#2489): an enumeration field of +``make()`` takes the member or an :class:`int` code, resolved with the +enumeration's ``get`` by ``Protocol._make_index``, and a name is refused rather +than resolved against an enumeration inferred from the caller's frame. #2617: a +code the field cannot carry, and a :obj:`bool` for a field wider than one bit, +raise :exc:`~pcapkit.utilities.exceptions.ProtocolError` rather than being +masked or written as a different value. + +Each row of :data:`FIELDS` is one field of one maker, reached as a caller reaches +it -- through ``make()``, and through its ``frame=``, ``body=`` or ``content=`` +for a frame, body or extension maker -- with the enumeration it holds and the +width of its field. ``make()`` is called directly, so that a value its own +checks let through cannot pass as refused because the parse of the octets +refuses them. + +""" +from __future__ import annotations + +import importlib +import importlib.util +import unittest +from typing import TYPE_CHECKING, NamedTuple +from unittest import mock + +from tests._support import reimport_once_per_class + +if TYPE_CHECKING: + from typing import Any, Callable + +HAS_PYCRATE = importlib.util.find_spec('pycrate_asn1dir') is not None + +#: The one cipher kind of the SSLv2 hellos, which the draft requires at least +#: one of: ``SSL_CK_DES_192_EDE3_CBC_WITH_MD5``. +CIPHER_SPECS = b'\x07\x00\xc0' + + +def _load(path: str) -> 'Any': + """The object ``path``, ``module:name``, names.""" + module, name = path.split(':') + return getattr(importlib.import_module(module), name) + + +def _make(protocol: str, **kwargs: 'Any') -> bytes: + """The octets ``make()`` of ``protocol``, ``module:class``, builds from ``kwargs``. + + Called on an instance that has parsed nothing, so that a value is refused by + ``make()`` itself rather than by the parse of what it built. + + """ + return bytes(object.__new__(_load(protocol)).make(**kwargs)) + + +def _member(path: str, name: str) -> 'Any': + """The member ``name`` of the enumeration ``path``, ``module:class``.""" + return getattr(_load(path), name) + + +OSPF = 'pcapkit.protocols.application.ospf:OSPF' +HTTPV1 = 'pcapkit.protocols.application.httpv1:HTTPv1' +HTTPV2 = 'pcapkit.protocols.application.httpv2:HTTPv2' +TLS = 'pcapkit.protocols.application.tls:TLS' +DTLS = 'pcapkit.protocols.application.dtls:DTLS' +SSLV2 = 'pcapkit.protocols.application.sslv2:SSLv2' +NGAP = 'pcapkit.protocols.application.ngap:NGAP' + +FRAME = 'pcapkit.const.http.frame:Frame' +SSLV2_TYPE = 'pcapkit.const.sslv2.handshake_type:HandshakeType' +PROCEDURE = 'pcapkit.const.ngap.procedure_code:ProcedureCode' + + +def _tls_extension(code: 'Any') -> bytes: + """A TLS record carrying a ``ClientHello`` whose one extension is a ``(code, {})`` pair.""" + from pcapkit.const.tls.content_type import ContentType + from pcapkit.const.tls.handshake_type import HandshakeType + from pcapkit.protocols.data.application.tls import ClientHello, Handshake + + hello = ClientHello(version=0x0303, random=bytes(32), session_id=b'', cookie=None, + cipher_suites=(0x1301,), compression_methods=(0,), extensions=((code, {}),)) + return _make(TLS, content_type=ContentType.handshake, + content=(Handshake(msg_type=HandshakeType.client_hello, length=0, body=hello),)) + + +def _hello(name: str, **body: 'Any') -> bytes: + """An SSLv2 record of the ``name`` hello, built from ``body``.""" + return _make(SSLV2, msg_type=_member(SSLV2_TYPE, name), body={'cipher_specs': CIPHER_SPECS, **body}) + + +def _ngap(**kwargs: 'Any') -> bytes: + """An ``NGSetupRequest`` with no IEs, built from ``kwargs``.""" + kwargs.setdefault('procedure', _member(PROCEDURE, 'NGSetup')) + return _make(NGAP, message='NGSetupRequest', value={'protocolIEs': []}, **kwargs) + + +class Field(NamedTuple): + """One enumeration field of one maker.""" + + #: The maker, by name. + maker: str + #: The field, as the maker names its argument. + field: str + #: The enumeration the field holds, as ``module:class``. + enum: str + #: Width of the field, in bits. + bits: int + #: Name of the member to build with. + member: str + #: Builds the packet's octets with the field set to its one argument. + build: 'Callable[[Any], bytes]' + #: Codes that fit the field and fall outside the enumeration's own range. + refused: 'tuple[int, ...]' = () + #: Module whose ``load_pycrate`` encodes the packet, for a maker that + #: must refuse a value before it reaches the encoder. + encoder: 'str | None' = None + + +FIELDS = ( + Field('OSPF.make', 'type', 'pcapkit.const.ospf.packet:Packet', 8, 'Link_State_Request', + lambda value: _make(OSPF, type=value)), + Field('OSPF.make', 'auth_type', 'pcapkit.const.ospf.authentication:Authentication', 16, + 'Simple_Password_Authentication', lambda value: _make(OSPF, auth_type=value)), + # NOTE: 10 bits hold every three-digit status code; StatusCode's range stops at 999. + Field('HTTPv1._make_start_line', 'status', 'pcapkit.const.http.status_code:StatusCode', 10, + 'CODE_404', lambda value: _make(HTTPV1, status=value), refused=(1000, 1023)), + Field('HTTPv2.make', 'type', FRAME, 8, 'WINDOW_UPDATE', + lambda value: _make(HTTPV2, type=value, sid=1)), + Field('HTTPv2._make_http_rst_stream', 'error', 'pcapkit.const.http.error_code:ErrorCode', 32, + 'CANCEL', + lambda value: _make(HTTPV2, type=_member(FRAME, 'RST_STREAM'), sid=1, frame={'error': value})), + Field('HTTPv2._make_http_goaway', 'error', 'pcapkit.const.http.error_code:ErrorCode', 32, + 'PROTOCOL_ERROR', + lambda value: _make(HTTPV2, type=_member(FRAME, 'GOAWAY'), frame={'error': value})), + Field('HTTPv2._make_http_settings', 'settings (list)', 'pcapkit.const.http.setting:Setting', 16, + 'MAX_FRAME_SIZE', + lambda value: _make(HTTPV2, type=_member(FRAME, 'SETTINGS'), frame={'settings': [(value, 16384)]})), + Field('HTTPv2._make_http_settings', 'settings (dict)', 'pcapkit.const.http.setting:Setting', 16, + 'MAX_FRAME_SIZE', + lambda value: _make(HTTPV2, type=_member(FRAME, 'SETTINGS'), frame={'settings': {value: 16384}})), + Field('TLS.make', 'content_type', 'pcapkit.const.tls.content_type:ContentType', 8, 'alert', + lambda value: _make(TLS, content_type=value, content=b'\x02\x28')), + Field('SSL._make_tls_extensions', 'extensions', 'pcapkit.const.tls.extension_type:ExtensionType', 16, + 'server_name', _tls_extension), + Field('DTLS.make', 'content_type', 'pcapkit.const.tls.content_type:ContentType', 8, 'alert', + lambda value: _make(DTLS, content_type=value, content=b'\x02\x28')), + Field('SSLv2.make', 'msg_type', SSLV2_TYPE, 8, 'CLIENT_FINISHED', + lambda value: _make(SSLV2, msg_type=value, body=b'\x00' * 16)), + Field('SSLv2._make_sslv2_client_hello', 'client_version', 'pcapkit.const.sslv2.version:Version', 16, + 'SSLv2', lambda value: _hello('CLIENT_HELLO', client_version=value)), + Field('SSLv2._make_sslv2_server_hello', 'server_version', 'pcapkit.const.sslv2.version:Version', 16, + 'SSLv2', lambda value: _hello('SERVER_HELLO', server_version=value)), + Field('NGAP.make', 'procedure', PROCEDURE, 8, 'NGSetup', + lambda value: _ngap(procedure=value), encoder='pcapkit.protocols.application.ngap'), + # NOTE: Criticality is an ENUMERATED of three, two bits in aligned PER. + Field('NGAP.make', 'criticality', 'pcapkit.protocols.application.ngap:Criticality', 2, 'reject', + lambda value: _ngap(criticality=value), encoder='pcapkit.protocols.application.ngap'), +) + + +class ApplicationMakeCodeTests(unittest.TestCase): + """Each field refuses what it cannot carry, and builds a member and its code alike.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + from pcapkit.utilities.exceptions import ProtocolError + + self.ProtocolError = ProtocolError + + def assertRefused(self, row: 'Field', value: 'Any') -> None: # pylint: disable=invalid-name + """``row``'s maker raises ProtocolError for ``value``, before any encoder runs.""" + if row.encoder is None: + with self.assertRaises(self.ProtocolError): + row.build(value) + return + # NOTE: the encoder's own failure is a ProtocolError too, so it must not be reached. + module = importlib.import_module(row.encoder) + with mock.patch.object(module, 'load_pycrate', side_effect=AssertionError('reached the encoder')), \ + self.assertRaises(self.ProtocolError): + row.build(value) + + def test_rows_name_real_members(self) -> None: + for row in FIELDS: + with self.subTest(maker=row.maker, field=row.field): + enum = _load(row.enum) + member = getattr(enum, row.member) + self.assertIsInstance(member, enum) + self.assertLess(int(member), 1 << row.bits) + + def test_code_past_the_field_is_refused(self) -> None: + for row in FIELDS: + for code in (1 << row.bits,) + row.refused: + with self.subTest(maker=row.maker, field=row.field, code=code): + self.assertRefused(row, code) + + def test_bool_is_refused_for_a_wider_field(self) -> None: + for row in FIELDS: + if row.bits > 1: + with self.subTest(maker=row.maker, field=row.field): + self.assertRefused(row, True) + + def test_name_is_refused(self) -> None: + for row in FIELDS: + with self.subTest(maker=row.maker, field=row.field, name=row.member): + self.assertRefused(row, row.member) + + def test_member_and_code_build_the_same_octets(self) -> None: + for row in FIELDS: + with self.subTest(maker=row.maker, field=row.field): + if row.encoder is not None and not HAS_PYCRATE: + self.skipTest('pycrate not installed') + member = _member(row.enum, row.member) + self.assertEqual(row.build(member), row.build(int(member))) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/protocols/application/test_ospf_checksum_2475_unit.py b/tests/protocols/application/test_ospf_checksum_2475_unit.py index e40812a493..762e509f25 100644 --- a/tests/protocols/application/test_ospf_checksum_2475_unit.py +++ b/tests/protocols/application/test_ospf_checksum_2475_unit.py @@ -54,12 +54,14 @@ class OSPFChecksumTests(unittest.TestCase): def setUp(self) -> None: reimport_once_per_class(self) + from pcapkit.const.ospf.packet import Packet from pcapkit.protocols.application.ospf import OSPF from pcapkit.corekit.checksum.internet import ChecksumStatus from pcapkit.utilities import config from pcapkit.utilities.warnings import ChecksumWarning, ProtocolWarning self.OSPF = OSPF + self.Hello = Packet.Hello self.Status = ChecksumStatus self.config = config self.ProtocolWarning = ProtocolWarning @@ -93,7 +95,7 @@ def test_a_corrupted_checksum_warns(self) -> None: def test_cryptographic_authentication_leaves_it_unused(self) -> None: packet = BUILT[0][:14] + b'\x00\x02' + bytes([0, 0, 1, 16]) + bytes(4) + HELLO self.assertEqual(self.status(packet), (self.Status.NOT_COMPUTED, [])) - made = self.OSPF(type='Hello', router_id='192.0.2.1', auth_type=2, + made = self.OSPF(type=self.Hello, router_id='192.0.2.1', auth_type=2, auth_data={'key_id': 1, 'len': 16, 'seq': 0}, payload=HELLO).data self.assertEqual(made[12:14], b'\x00\x00') @@ -115,7 +117,7 @@ def test_parse_rebuild_is_exact(self) -> None: def test_make_computes_the_checksum(self) -> None: for packet, kwargs in zip(BUILT, ({}, {'auth_type': 1, 'auth_data': b'secret!!'})): with self.subTest(kwargs=kwargs): - made = self.OSPF(type='Hello', router_id='192.0.2.1', payload=HELLO, **kwargs).data + made = self.OSPF(type=self.Hello, router_id='192.0.2.1', payload=HELLO, **kwargs).data self.assertEqual(made, packet) ospf, messages = self.parse(made) self.assertEqual((ospf.info.checksum_status, messages), (self.Status.VALID, [])) @@ -125,7 +127,7 @@ def test_make_computes_the_checksum(self) -> None: def test_make_agrees_with_scapy(self) -> None: from scapy.contrib.ospf import OSPF_Hdr - made = self.OSPF(type='Hello', router_id='192.0.2.1', payload=HELLO).data + made = self.OSPF(type=self.Hello, router_id='192.0.2.1', payload=HELLO).data packet = OSPF_Hdr(made) del packet.chksum self.assertEqual(bytes(packet), made) diff --git a/tests/protocols/application/test_tls_extensions_2514_unit.py b/tests/protocols/application/test_tls_extensions_2514_unit.py index 807c84f007..2e391d5585 100644 --- a/tests/protocols/application/test_tls_extensions_2514_unit.py +++ b/tests/protocols/application/test_tls_extensions_2514_unit.py @@ -389,12 +389,11 @@ def test_a_value_too_wide_for_its_field_raises_struct_error(self) -> None: self._content(alias, content_type, content) # The record header, which the schema would otherwise mask to its width. + # NOTE: Not its content type, an enumeration field refused with ProtocolError (#2594). for name, alias, kwargs in ( - ('record content_type', 'TLS', {'content_type': 0x100}), ('record version', 'TLS', {'version': 0x10000}), ('record negative version', 'TLS', {'version': -1}), ('record length', 'TLS', {'data': bytes(0x10000)}), - ('DTLS record content_type', 'DTLS', {'content_type': 0x100}), ('DTLS record version', 'DTLS', {'version': 0x10000}), ('DTLS record epoch', 'DTLS', {'epoch': 0x10000}), ('DTLS record sequence_number', 'DTLS', {'sequence_number': 1 << 48}), diff --git a/tests/protocols/internet/test_hip_list_argument_str_names_2394_unit.py b/tests/protocols/internet/test_hip_list_argument_str_names_2394_unit.py index c3684c3e9b..05e72434e7 100644 --- a/tests/protocols/internet/test_hip_list_argument_str_names_2394_unit.py +++ b/tests/protocols/internet/test_hip_list_argument_str_names_2394_unit.py @@ -1,28 +1,23 @@ # -*- coding: utf-8 -*- -"""A HIP list argument resolves a :obj:`str` enumeration member name. - -GitHub issue #2394, the third of the family #2356 and #2393 opened. -:meth:`~pcapkit.protocols.protocol.ProtocolBase._make_index` infers the -enumeration a bare member name belongs to from the *default value* of the -``make()`` argument the name arrived as -- so an argument whose default is an -enumeration member resolves a name on its own, and one whose default is -:data:`None` has nothing to read. - -Some HIP parameters carry a *list* of enumeration values rather than one, and -build it in a loop. The value handed to ``_make_index`` is therefore the loop -variable, which is no argument of the maker at all, so the inference cannot -reach the list argument's annotation and a name goes unresolved however the -list argument is declared. Measured before the fix: the member and integer -forms of every one of them built the correct octets, and the member-name -form raised ``ProtocolNotImplemented: protocol 'RENDEZVOUS' not implemented``. -Each maker now names its own enumeration, which is what #2393 did for the -scalar case -- just ahead of the loop rather than at the call, so that the -decision is taken once per parameter instead of once per item. +"""A HIP list argument refuses a :obj:`str` enumeration member name. + +GitHub issue #2394, the third of the family #2356 and #2393 opened, and settled +by #2594. Some HIP parameters carry a *list* of enumeration values rather than +one, and build it item by item. A list item is no ``make()`` argument, so no +enumeration can be inferred for a name in one from an argument's default (#2394). + +Under #2594 nothing is inferred: every item of these lists is a member or an +:class:`int` code, resolved with the enumeration's ``get``, and a name is +refused with :exc:`~pcapkit.utilities.exceptions.ProtocolError` wherever it sits +in the list. A caller holding a name resolves it first, with +:meth:`~pcapkit.protocols.protocol.Protocol.resolve_index`, which takes the +name, default, namespace and reversed group explicitly; resolved one item at a +time, it builds the octets the member does. :meth:`HIPListArgumentStrNameTests.test_every_list_argument_maker_has_a_case` -reads the loop-variable sites off ``hip.py`` itself rather than trusting the -list below, so a further parameter added with the same shape cannot land -without a case here. +reads the list-item sites off ``hip.py`` itself rather than trusting the table +below, so a further parameter added with the same shape cannot land without a +case here. Every case builds its own packet in memory and reads no capture. :class:`HIP` and the enumerations are imported inside each test, after @@ -37,9 +32,13 @@ import inspect import textwrap import unittest +from typing import TYPE_CHECKING from tests._support import reimport_once_per_class +if TYPE_CHECKING: + from typing import Any + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -60,8 +59,8 @@ #: One case per maker: ``{maker: (parameter name, list keyword, enum module and #: class, member name, the other keywords the parameter needs)}``. Each member is #: a non-zero one the maker would not arrive at on its own, so an octet string -#: carrying it is evidence the name resolved rather than that a default survived. -#: Keyed by maker rather than by parameter because +#: carrying it is evidence the item reached the wire rather than that a default +#: survived. Keyed by maker rather than by parameter because #: :meth:`HIPListArgumentStrNameTests.test_every_list_argument_maker_has_a_case` #: compares these keys against the makers it finds in ``hip.py``. CASES = { @@ -90,9 +89,8 @@ 'REG_RESPONSE', 'reg_response', 'pcapkit.const.hip.registration.Registration', 'RELAY_UDP_HIP', {'lifetime': 42}), # ``reg_failed`` takes registration types, not failure types (#2309); the - # sibling ``failure_type`` is a scalar whose own default is a member, so it - # resolved a name all along and is pinned by - # :meth:`test_a_scalar_sibling_still_resolves_by_inference`. + # scalar sibling ``failure_type`` is covered by + # :meth:`test_a_scalar_sibling_takes_a_resolved_name_too`. '_make_param_reg_failed': ( 'REG_FAILED', 'reg_failed', 'pcapkit.const.hip.registration.Registration', 'RENDEZVOUS', {'failure_type': 1}), @@ -112,7 +110,7 @@ } -def _attr(path: 'str') -> 'object': +def _attr(path: 'str') -> 'Any': """Import ``path`` and return the attribute it names. Args: @@ -131,7 +129,7 @@ def _attr(path: 'str') -> 'object': @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class HIPListArgumentStrNameTests(unittest.TestCase): - """A member name in a list argument resolves against its enumeration.""" + """A member name in a list argument is refused; a resolved one builds.""" def setUp(self) -> None: reimport_once_per_class(self) @@ -155,42 +153,64 @@ def _build(code: 'object', value: 'object') -> 'bytes': # ``__post_init__`` assigns only once ``read`` has returned (#1184). base['version'] = HIP_VERSION.get(int(code), 2) # type: ignore[call-overload] return HIP(parameters=[(code, value)], extension=True, - **base).data # type: ignore[arg-type] + **base).data + + def test_a_member_name_in_a_list_is_refused(self) -> None: + """#2594: a name in a list raises ``ProtocolError``, for every maker. + + Each maker names its enumeration at the call (#2394), and resolves a + list item against it as a member or a code, never as a name. + + """ + error = _attr('pcapkit.utilities.exceptions.ProtocolError') + parameter = _attr('pcapkit.const.hip.parameter.Parameter') + + for maker, (name, keyword, _, member, extra) in CASES.items(): + with self.subTest(maker=maker): + with self.assertRaises(error): + self._build(getattr(parameter, name), {**extra, keyword: [member]}) - def test_a_member_name_in_a_list_builds_what_the_member_builds(self) -> None: - """#2394: the three forms of a list item agree, octet for octet. + def test_resolve_index_builds_what_the_member_builds(self) -> None: + """#2594: a name resolved first builds the member's octets, item by item. - The member-name form raised ``ProtocolNotImplemented`` for every maker - in ``CASES`` before the fix, while the member and integer forms built - these very octets, so this is the fail-without-fix assertion and the - no-regression one at once. + The member, its :class:`int` code and the name passed through + :meth:`~pcapkit.protocols.protocol.Protocol.resolve_index` agree octet for + octet, and none of them is what an absent list builds. """ + from pcapkit.protocols.protocol import Protocol + parameter = _attr('pcapkit.const.hip.parameter.Parameter') for maker, (name, keyword, enum_path, member, extra) in CASES.items(): with self.subTest(maker=maker): code = getattr(parameter, name) - value = getattr(_attr(enum_path), member) + namespace = _attr(enum_path) + value = getattr(namespace, member) expected = self._build(code, {**extra, keyword: [value]}) - self.assertEqual(self._build(code, {**extra, keyword: [member]}).hex(), + resolved = Protocol.resolve_index(member, namespace=namespace) + self.assertIs(resolved, value) + self.assertEqual(self._build(code, {**extra, keyword: [resolved]}).hex(), expected.hex()) self.assertEqual(self._build(code, {**extra, keyword: [int(value)]}).hex(), expected.hex()) # the member is not what an absent list produces, so an item - # that never arrived cannot pass as one that resolved + # that never arrived cannot pass as one that was written self.assertNotEqual(self._build(code, dict(extra)).hex(), expected.hex()) - def test_a_list_resolves_every_item_it_holds(self) -> None: - """#2394: a name resolves wherever it sits, mixed in with other forms. + def test_a_list_is_checked_item_by_item(self) -> None: + """#2594: a name is refused wherever it sits, mixed in with other forms. - The fix is inside the loop rather than ahead of it, so a list is not - resolved as a whole: a name in the second position has to resolve as - readily as one in the first, and alongside the member and integer forms. + A list is not checked as a whole: a name in the second position is + refused as readily as one in the first, and the same list with that item + resolved builds what the members build. """ + from pcapkit.protocols.protocol import Protocol + + error = _attr('pcapkit.utilities.exceptions.ProtocolError') parameter = _attr('pcapkit.const.hip.parameter.Parameter') transport = _attr('pcapkit.const.hip.transport.Transport') @@ -198,45 +218,59 @@ def test_a_list_resolves_every_item_it_holds(self) -> None: expected = self._build(code, {'port': 1234, 'modes': [ transport.DEFAULT, transport.ESP, transport.ESP_TCP]}) - self.assertEqual( - self._build(code, {'port': 1234, 'modes': [1, 'ESP', transport.ESP_TCP]}).hex(), - expected.hex()) - self.assertEqual( - self._build(code, {'port': 1234, 'modes': ['DEFAULT', 'ESP', 'ESP_TCP']}).hex(), - expected.hex()) + with self.assertRaises(error): + self._build(code, {'port': 1234, 'modes': [1, 'ESP', transport.ESP_TCP]}) - def test_an_explicit_namespace_still_wins(self) -> None: - """A ``*_namespace`` keyword overrides the enumeration the maker names. + resolved = [Protocol.resolve_index(item, namespace=transport) + for item in (1, 'ESP', transport.ESP_TCP)] + self.assertEqual(self._build(code, {'port': 1234, 'modes': resolved}).hex(), + expected.hex()) - The fix names an enumeration only where the caller named none, so that - the documented ``*_namespace`` keyword keeps resolving a name the - parameter's own enumeration does not carry. + def test_resolve_index_takes_the_group_explicitly(self) -> None: + """#2594: ``resolve_index`` takes the name, default, namespace and reversed group. + + Each case builds the octets the member builds: a name looked up in + another enumeration, a ``str -> int`` mapping with ``reversed``, and a + ``default`` for a name the namespace lacks. A name with neither a match + nor a ``default`` raises ``ProtocolNotImplemented``. """ + from pcapkit.protocols.protocol import Protocol + + missing = _attr('pcapkit.utilities.exceptions.ProtocolNotImplemented') parameter = _attr('pcapkit.const.hip.parameter.Parameter') + group = _attr('pcapkit.const.hip.group.Group') registration = _attr('pcapkit.const.hip.registration.Registration') + def build(item: 'object') -> 'str': + return self._build(parameter.DH_GROUP_LIST, {'groups': [item]})[40:].hex() + # ``RENDEZVOUS`` is a registration type, not a Diffie-Hellman group - octets = self._build(parameter.DH_GROUP_LIST, - {'groups': ['RENDEZVOUS'], 'group_namespace': registration}) - self.assertEqual(octets[40:].hex(), '01ff000101000000') + self.assertEqual(build(Protocol.resolve_index('RENDEZVOUS', namespace=registration)), + '01ff000101000000') + self.assertEqual(build(Protocol.resolve_index('mine', namespace={'mine': 9}, + reversed=True)), + '01ff000109000000') + self.assertEqual(build(Protocol.resolve_index('No_Such_Group', 4, namespace=group)), + '01ff000104000000') - # a ``str -> int`` mapping needs ``*_reversed``, and still resolves - octets = self._build(parameter.DH_GROUP_LIST, - {'groups': ['mine'], 'group_namespace': {'mine': 9}, - 'group_reversed': True}) - self.assertEqual(octets[40:].hex(), '01ff000109000000') + with self.assertRaises(missing): + Protocol.resolve_index('No_Such_Group', namespace=group) - def test_a_scalar_sibling_still_resolves_by_inference(self) -> None: - """``REG_FAILED``'s ``failure_type`` resolves from its own default. + def test_a_scalar_sibling_takes_a_resolved_name_too(self) -> None: + """``REG_FAILED``'s ``failure_type`` refuses a name, as its list does. It is a scalar whose default is a :class:`~pcapkit.const.hip.registration_failure.RegistrationFailure` - member, so the inference #2394 cannot perform for a list item works for - it, and the two arguments resolve against *different* enumerations in + member, a shape an inference from argument defaults would resolve; #2594 + refuses the name there as well. Resolved against their own + enumerations, the two arguments build the octets their members build in the same call. """ + from pcapkit.protocols.protocol import Protocol + + error = _attr('pcapkit.utilities.exceptions.ProtocolError') parameter = _attr('pcapkit.const.hip.parameter.Parameter') failure = _attr('pcapkit.const.hip.registration_failure.RegistrationFailure') registration = _attr('pcapkit.const.hip.registration.Registration') @@ -245,39 +279,25 @@ def test_a_scalar_sibling_still_resolves_by_inference(self) -> None: 'failure_type': failure.Registration_type_unavailable, 'reg_failed': [registration.RENDEZVOUS], }) + with self.assertRaises(error): + self._build(parameter.REG_FAILED, { + 'failure_type': 'Registration_type_unavailable', + 'reg_failed': [registration.RENDEZVOUS], + }) self.assertEqual(self._build(parameter.REG_FAILED, { - 'failure_type': 'Registration_type_unavailable', - 'reg_failed': ['RENDEZVOUS'], + 'failure_type': Protocol.resolve_index('Registration_type_unavailable', + namespace=failure), + 'reg_failed': [Protocol.resolve_index('RENDEZVOUS', namespace=registration)], }).hex(), expected.hex()) self.assertEqual(expected[40:].hex(), '03a8000201010000') - def test_an_unresolvable_name_still_raises_or_falls_back(self) -> None: - """Naming the enumeration does not make every string resolve. - - The enumeration is named, not bypassed, so a string that is no member - of it still raises -- or takes the ``*_default`` the caller gave, which - is the documented escape. - - """ - exc = _attr('pcapkit.utilities.exceptions.ProtocolNotImplemented') - parameter = _attr('pcapkit.const.hip.parameter.Parameter') - - with self.assertRaises(exc): - self._build(parameter.DH_GROUP_LIST, {'groups': ['No_Such_Group']}) - - octets = self._build(parameter.DH_GROUP_LIST, - {'groups': ['No_Such_Group'], 'group_default': 4}) - self.assertEqual(octets[40:].hex(), '01ff000104000000') - def test_every_list_argument_maker_has_a_case(self) -> None: - """Every loop-variable ``_make_index`` site in ``hip.py`` has a case. + """Every list-item ``_make_index`` site in ``hip.py`` has a case. - The set is read off the makers rather than listed, because the bug is a - property of the *shape* of the call: the value handed to - :meth:`~pcapkit.protocols.protocol.ProtocolBase._make_index` is a name - that is not an argument of the maker, so nothing is there for the - namespace to be inferred from. A further parameter of that shape - would be broken in exactly the same way and is caught here. + The set is read off the makers rather than listed: a site is a call to + :meth:`~pcapkit.protocols.protocol.ProtocolBase._make_index` whose value + is a name that is not an argument of the maker, the item of a list + argument. A further parameter of that shape gets a case here. """ from pcapkit.protocols.internet.hip import HIP @@ -300,20 +320,17 @@ def test_every_list_argument_maker_has_a_case(self) -> None: found.add(name) self.assertEqual(found, set(CASES), - 'a HIP maker resolves a name that is not one of its own ' - 'arguments and has no case in CASES (#2394)') + 'a HIP maker resolves a list item and has no case in CASES (#2394)') def test_the_named_enumerations_are_imported_at_runtime(self) -> None: - """#2394: the enumerations the fix needs are no longer type-only. + """#2394: the enumerations the list makers resolve against are runtime names. ``Suite``, ``NATTraversal``, ``Registration``, ``ESPTransformSuite`` and - ``Transport`` were imported under ``TYPE_CHECKING`` alone, which is why - the fix is not a one-line patch. They sit in - :mod:`pcapkit.const.hip`, which imports nothing from - :mod:`pcapkit.protocols`, so naming them at runtime closes no cycle -- - their sibling - :class:`~pcapkit.const.hip.registration_failure.RegistrationFailure` was - imported at runtime all along. + ``Transport`` are imported at module level rather than under + ``TYPE_CHECKING`` alone (#2394), since each list maker passes its + enumeration to ``_make_index`` at run time. + They sit in :mod:`pcapkit.const.hip`, which imports nothing from + :mod:`pcapkit.protocols`, so naming them at runtime closes no cycle. """ from pcapkit.protocols.internet import hip diff --git a/tests/protocols/internet/test_hip_make_codes_2594_unit.py b/tests/protocols/internet/test_hip_make_codes_2594_unit.py new file mode 100644 index 0000000000..91a2261060 --- /dev/null +++ b/tests/protocols/internet/test_hip_make_codes_2594_unit.py @@ -0,0 +1,388 @@ +# -*- coding: utf-8 -*- +"""Each enumeration field of the HIP makers takes a member or a code, and nothing else. + +GitHub issue #2594, carrying out the ruling given in review of #2593: a field of +``make()`` that holds an enumeration takes the member or an :class:`int` code, +resolved with the enumeration's ``get``, and no maker takes a ``*_default``, +``*_namespace`` or ``*_reversed`` keyword. #2617 adds that a code too wide for +its field, or a :obj:`bool` for a field wider than one bit, raises +``ProtocolError`` rather than being masked or written as another value. + +:data:`ROWS` holds one row per (maker, field, enumeration, width) of +:class:`~pcapkit.protocols.internet.hip.HIP`. Each width is the field's in +:mod:`pcapkit.protocols.schema.internet.hip` -- ``EnumField(length=1)`` is 8 +bits, ``length=2`` is 16, a bit-field item its declared bits -- which matches +the RFC each parameter cites. Every row is reached the way a caller reaches it, +through ``HIP(parameters=[(code, value)])`` or the header keyword itself, and +asserts: + +* ``1 << bits`` raises ``ProtocolError`` naming the field; +* :data:`True` raises ``ProtocolError`` (no field here is one bit wide); +* the member's name as a :obj:`str` raises ``ProtocolError``; +* the member and ``int(member)`` build identical octets, and not the octets the + maker builds without the argument; +* ``(1 << bits) - 1`` builds, parses back and rebuilds byte for byte, also + where no member holds it and the enumeration's own range is narrower than the + field, as for the mode IDs of ``HIP_TRANSPORT_MODE`` and the suite IDs of + ``HIT_SUITE_LIST``, whose parsed values :data:`PARSED` gives back to ``make``. + +The type of each parameter is such a field in both forms ``parameters`` takes +it in, a ``(code, value)`` pair and the key of parsed data, and refuses a +negative code there too. + +:meth:`HIPMakeCodeTests.test_every_enumeration_argument_has_a_row` reads the +enumeration arguments off the makers' signatures, so a further one cannot land +without a row. + +Every case builds its own packet in memory and reads no capture. :class:`HIP` +and the enumerations are imported inside each test, after +:func:`~tests._support.reimport_once_per_class`. + +""" +# Deferring these is the design: each build runs after the per-class re-import. +# pylint: disable=import-outside-toplevel +from __future__ import annotations + +import importlib +import importlib.util +import inspect +import io +import unittest +from typing import TYPE_CHECKING + +from tests._support import reimport_once_per_class + +if TYPE_CHECKING: + from typing import Any, Callable + +RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') +HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) + +#: ``make.next`` and ``make.packet`` are header keywords of :meth:`HIP.make`, and +#: ``make.parameters`` is the code of a ``(code, value)`` pair, which +#: ``_make_hip_param`` resolves before it dispatches to a parameter maker. +HEADER = 'make' + +#: ``(maker, argument, enumeration, member, bits, field name, parameter code, +#: other arguments, list)``: one row per enumeration field. ``parameter code`` is +#: the parameter carrying the field, or :data:`None` for a header keyword; a +#: ``list`` row puts the value in a one-item list. Each member is one the maker +#: would not arrive at on its own. +ROWS = ( + (HEADER, 'next', 'pcapkit.const.reg.transtype.TransType', 'TCP', 8, + 'next header', None, {}, False), + (HEADER, 'packet', 'pcapkit.const.hip.packet.Packet', 'I2', 7, + 'packet type', None, {}, False), + (HEADER, 'parameters', 'pcapkit.const.hip.parameter.Parameter', 'SEQ', 16, + 'parameter type', None, {'update_id': 1}, False), + ('_make_param_dh_group_list', 'groups', 'pcapkit.const.hip.group.Group', + 'Group_3072_bit_MODP_group', 8, 'DH group ID', 'DH_GROUP_LIST', {}, True), + ('_make_param_diffie_hellman', 'group', 'pcapkit.const.hip.group.Group', + 'Group_3072_bit_MODP_group', 8, 'group ID', 'DIFFIE_HELLMAN', {'pub_val': 1}, False), + ('_make_param_hip_transform', 'suites', 'pcapkit.const.hip.suite.Suite', + 'BLOWFISH_CBC_with_HMAC_SHA1', 16, 'suite ID', 'HIP_TRANSFORM', {}, True), + ('_make_param_hip_cipher', 'ciphers', 'pcapkit.const.hip.cipher.Cipher', + 'AES_256_CBC', 16, 'cipher ID', 'HIP_CIPHER', {}, True), + ('_make_param_nat_traversal_mode', 'modes', 'pcapkit.const.hip.nat_traversal.NATTraversal', + 'ICE_HIP_UDP', 16, 'mode ID', 'NAT_TRAVERSAL_MODE', {}, True), + # ``ENCRYPTED`` carries no cipher ID of its own; the cipher decides whether + # the IV is written, and its width is the one ``HIP_CIPHER`` carries it in. + ('_make_param_encrypted', 'cipher', 'pcapkit.const.hip.cipher.Cipher', + 'AES_256_CBC', 16, 'cipher ID', 'ENCRYPTED', {'iv': bytes(16), 'data': b'x'}, False), + # ``hi`` as octets, since a curve sets the algorithm itself. + ('_make_param_host_id', 'hi_algorithm', 'pcapkit.const.hip.hi_algorithm.HIAlgorithm', + 'RSA', 16, 'algorithm', 'HOST_ID', {'hi': b'\x01\x02'}, False), + ('_make_param_host_id', 'di_type', 'pcapkit.const.hip.di.DITypes', + 'FQDN', 4, 'DI-type', 'HOST_ID', {'hi': b'\x01\x02', 'di': b'host'}, False), + ('_make_param_hit_suite_list', 'suites', 'pcapkit.const.hip.hit_suite.HITSuite', + 'ECDSA_SHA_384', 8, 'HIT suite ID', 'HIT_SUITE_LIST', {}, True), + ('_make_param_cert', 'cert_type', 'pcapkit.const.hip.certificate.Certificate', + 'Hash_and_URL_of_X_509_v3', 8, 'certificate type', 'CERT', {}, False), + ('_make_param_notification', 'msg_type', 'pcapkit.const.hip.notify_message.NotifyMessage', + 'INVALID_SYNTAX', 16, 'notify message type', 'NOTIFICATION', {}, False), + ('_make_param_reg_info', 'reg_info', 'pcapkit.const.hip.registration.Registration', + 'RELAY_UDP_ESP', 8, 'registration type', 'REG_INFO', + {'min_lifetime': 5, 'max_lifetime': 200}, True), + ('_make_param_reg_request', 'reg_request', 'pcapkit.const.hip.registration.Registration', + 'CANDIDATE_DISCOVERY', 8, 'registration type', 'REG_REQUEST', {'lifetime': 42}, True), + ('_make_param_reg_response', 'reg_response', 'pcapkit.const.hip.registration.Registration', + 'RELAY_UDP_HIP', 8, 'registration type', 'REG_RESPONSE', {'lifetime': 42}, True), + ('_make_param_reg_failed', 'failure_type', + 'pcapkit.const.hip.registration_failure.RegistrationFailure', + 'Insufficient_resources', 8, 'failure type', 'REG_FAILED', {}, False), + ('_make_param_reg_failed', 'reg_failed', 'pcapkit.const.hip.registration.Registration', + 'RENDEZVOUS', 8, 'registration type', 'REG_FAILED', {}, True), + ('_make_param_reg_from', 'protocol', 'pcapkit.const.reg.transtype.TransType', + 'TCP', 8, 'protocol', 'REG_FROM', {}, False), + ('_make_param_transport_format_list', 'formats', 'pcapkit.const.hip.parameter.Parameter', + 'ESP_TRANSFORM', 16, 'transport format type', 'TRANSPORT_FORMAT_LIST', {}, True), + ('_make_param_esp_transform', 'suites', + 'pcapkit.const.hip.esp_transform_suite.ESPTransformSuite', + 'AES_128_CBC_with_HMAC_SHA1', 16, 'suite ID', 'ESP_TRANSFORM', {}, True), + ('_make_param_payload_mic', 'next', 'pcapkit.const.reg.transtype.TransType', + 'TCP', 8, 'next header', 'PAYLOAD_MIC', {}, False), + ('_make_param_hip_transport_mode', 'modes', 'pcapkit.const.hip.transport.Transport', + 'ESP_TCP', 16, 'mode ID', 'HIP_TRANSPORT_MODE', {}, True), + ('_make_param_hip_signature_2', 'algorithm', 'pcapkit.const.hip.hi_algorithm.HIAlgorithm', + 'RSA', 16, 'signature algorithm', 'HIP_SIGNATURE_2', {}, False), + ('_make_param_hip_signature', 'algorithm', 'pcapkit.const.hip.hi_algorithm.HIAlgorithm', + 'RSA', 16, 'signature algorithm', 'HIP_SIGNATURE', {}, False), + ('_make_param_relay_from', 'protocol', 'pcapkit.const.reg.transtype.TransType', + 'TCP', 8, 'protocol', 'RELAY_FROM', {}, False), + ('_make_param_relay_to', 'protocol', 'pcapkit.const.reg.transtype.TransType', + 'TCP', 8, 'protocol', 'RELAY_TO', {}, False), +) + +#: Enumeration arguments with no row, and why. +EXEMPT = { + # Member-only: the curve's class picks the algorithm and the curve + # registry, so an ``int`` names no enumeration to resolve against. + '_make_param_host_id.hi_curve', +} + +#: ``HIP_TRANSFORM`` is :rfc:`5201` only; every other parameter here is built +#: at version 2. +VERSION = {'HIP_TRANSFORM': 1} + +#: Other arguments a row's widest code needs: a cipher ID no member holds +#: carries no IV. +WIDEST_EXTRA = { + '_make_param_encrypted.cipher': {'iv': None}, +} + +#: ``maker.argument`` to the attribute its parameter's data model keeps the +#: parsed values in, for the rows whose enumeration is narrower than the field. +PARSED = { + '_make_param_hip_transport_mode.modes': 'mode_id', + '_make_param_hit_suite_list.suites': 'suite_id', +} + + +def _attr(path: 'str') -> 'Any': + """Import ``path`` and return the attribute it names. + + Args: + path: dotted path whose last component is an attribute of the module + the rest of it names + + Returns: + The named attribute. + + """ + module, _, name = path.rpartition('.') + return getattr(importlib.import_module(module), name) + + +@unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') +class HIPMakeCodeTests(unittest.TestCase): + """An enumeration field takes a member or a code it can carry, nothing else.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + @staticmethod + def _builder(argument: 'str', code: 'Any', extra: 'dict[str, Any]', + listed: 'bool') -> 'tuple[int, Callable[..., bytes]]': + """The HIP version a row builds at, and a builder for its value. + + Args: + argument: the row's argument + code: name of the parameter carrying the field, or :data:`None` + extra: the other arguments the row needs + listed: if the argument takes a list + + Returns: + The version, and a function building the packet's octets from a + value, or from no value at all when given :data:`None`. + + """ + from pcapkit.const.hip.parameter import Parameter + from pcapkit.protocols.internet.hip import HIP + + version = VERSION.get(code, 2) + + def build(value: 'Any' = None) -> 'bytes': + # ``extension=True`` keeps ``HIP.alias`` off ``self._info``, which + # ``__post_init__`` assigns only once ``read`` has returned (#1184). + if code is None and argument == 'parameters' and value is not None: + return HIP(version=version, parameters=[(value, dict(extra))], + extension=True).data + if code is None: + kwargs = {} if value is None else {argument: value} + return HIP(version=version, extension=True, **kwargs).data + args = dict(extra) + if value is not None: + args[argument] = [value] if listed else value + return HIP(version=version, parameters=[(Parameter[code], args)], + extension=True).data + + return version, build + + def _rows(self) -> 'list[tuple[str, Any, int, str, int, Callable[..., bytes]]]': + """``(label, member, bits, field name, version, build)`` for each row.""" + rows = [] + for maker, argument, enum_path, member, bits, name, code, extra, listed in ROWS: + version, build = self._builder(argument, code, extra, listed) + rows.append((f'{maker}.{argument}', getattr(_attr(enum_path), member), + bits, name, version, build)) + return rows + + @staticmethod + def _parse(octets: 'bytes') -> 'Any': + """The HIP packet ``octets`` parse into.""" + from pcapkit.protocols.internet.hip import HIP + + return HIP(io.BytesIO(octets), len(octets), extension=True) + + def assertRefused(self, message: 'str', build: 'Callable[..., bytes]', value: 'Any') -> None: + """``build(value)`` raises a ``ProtocolError`` saying exactly ``message``. + + Any other outcome fails by assertion, naming what happened instead, so + that a value written or raised as something else reads as a failure + rather than as an error in the test. + + """ + from pcapkit.utilities.exceptions import ProtocolError + + try: + build(value) + except ProtocolError as error: + self.assertEqual(str(error), message) + except Exception as error: # pylint: disable=broad-except + self.fail(f'{value!r} raised {type(error).__name__}: {error}; ' + f'expected ProtocolError: {message}') + else: + self.fail(f'{value!r} was accepted; expected ProtocolError: {message}') + + def test_a_code_too_wide_for_its_field_is_refused(self) -> None: + """#2617: ``1 << bits`` raises ``ProtocolError`` naming the field.""" + for label, _, bits, name, version, build in self._rows(): + with self.subTest(row=label, bits=bits): + self.assertRefused(f'HIPv{version}: invalid {name}: {1 << bits!r}', + build, 1 << bits) + + def test_a_bool_is_refused(self) -> None: + """#2617: no field here is one bit wide, so :data:`True` is refused.""" + for label, _, bits, name, version, build in self._rows(): + self.assertGreater(bits, 1, label) + with self.subTest(row=label): + self.assertRefused(f'HIPv{version}: invalid {name}: True', build, True) + + def test_a_member_name_is_refused(self) -> None: + """#2594: a name is refused rather than resolved against an inferred enumeration.""" + for label, member, _, name, version, build in self._rows(): + with self.subTest(row=label): + self.assertRefused(f'HIPv{version}: invalid {name}: {member.name!r}', + build, member.name) + + def test_the_member_and_its_code_build_the_same_octets(self) -> None: + """#2594: the member and ``int(member)`` resolve with ``get`` to one field value.""" + for label, member, _, _, _, build in self._rows(): + with self.subTest(row=label): + expected = build(member) + self.assertEqual(build(int(member)).hex(), expected.hex()) + # the member is not what the maker builds without it, so a + # value that never reached the packet cannot pass + self.assertNotEqual(build().hex(), expected.hex()) + + def test_the_widest_code_builds_parses_back_and_rebuilds(self) -> None: + """#2594: the field width decides what is refused, so ``(1 << bits) - 1`` builds.""" + from pcapkit.protocols.internet.hip import HIP + + for maker, argument, _, _, bits, _, code, extra, listed in ROWS: + label = f'{maker}.{argument}' + _, build = self._builder(argument, code, {**extra, **WIDEST_EXTRA.get(label, {})}, + listed) + with self.subTest(row=label, code=(1 << bits) - 1): + octets = build((1 << bits) - 1) + self.assertEqual(HIP.from_data(self._parse(octets).info).data.hex(), octets.hex()) + + def test_a_parsed_code_builds_the_same_octets(self) -> None: + """#2594: ``make`` takes the codes a parse hands back, where no member holds them too.""" + from pcapkit.const.hip.parameter import Parameter + + rows = {f'{maker}.{argument}': (bits, code, extra) + for maker, argument, _, _, bits, _, code, extra, _ in ROWS} + for label, attribute in PARSED.items(): + bits, code, extra = rows[label] + _, build = self._builder(label.partition('.')[2], code, extra, False) + with self.subTest(row=label): + octets = build([(1 << bits) - 1, 1]) + info = self._parse(octets).info + parsed = list(getattr(info.parameters[getattr(Parameter, str(code))], attribute)) + self.assertEqual([int(value) for value in parsed], [(1 << bits) - 1, 1]) + self.assertEqual(build(parsed).hex(), octets.hex()) + + def _parameter_builders(self) -> 'dict[str, Callable[[Any], bytes]]': + """A builder for each form ``parameters`` takes a parameter's type in.""" + from pcapkit.const.hip.parameter import Parameter + from pcapkit.corekit.multidict import OrderedMultiDict + from pcapkit.protocols.internet.hip import HIP + + def pair(value: 'Any') -> 'bytes': + return HIP(version=2, parameters=[(value, {'contents': b'\x01'})], + extension=True).data + + # the parsed data of an unassigned parameter, which any code takes + _, param = list(self._parse(pair(0xFFFF)).info.parameters.items(multi=True))[0] + self.assertEqual(param.type, Parameter.get(0xFFFF)) + return { + 'pair': pair, + 'parsed': lambda value: HIP(version=2, parameters=OrderedMultiDict([(value, param)]), + extension=True).data, + } + + def test_a_parameter_type_the_field_cannot_carry_is_refused(self) -> None: + """#2594: the type of a parameter is checked before anything reads it as a registry key.""" + for form, build in self._parameter_builders().items(): + for value in (1 << 16, -1, True, 'SEQ'): + with self.subTest(form=form, value=value): + self.assertRefused(f'HIPv2: invalid parameter type: {value!r}', build, value) + + def test_the_widest_parameter_type_builds_parses_back_and_rebuilds(self) -> None: + """#2594: a parameter type no member holds builds in either form, and rebuilds.""" + from pcapkit.protocols.internet.hip import HIP + + for form, build in self._parameter_builders().items(): + with self.subTest(form=form): + octets = build(0xFFFF) + info = self._parse(octets).info + self.assertEqual([int(code) for code in info.parameters], [0xFFFF]) + self.assertEqual(HIP.from_data(info).data.hex(), octets.hex()) + + def test_every_enumeration_argument_has_a_row(self) -> None: + """Each enumeration-typed argument of :meth:`HIP.make` and the parameter makers has a row. + + Read off the signatures rather than listed, so a further enumeration + argument gets a row here or an entry in :data:`EXEMPT`. A parameter + maker's positional ``code`` is the code ``make.parameters`` resolves. + + """ + from pcapkit.protocols.internet.hip import HIP + + found = set() + for name in vars(HIP): + if name != 'make' and not name.startswith('_make_param_'): + continue + for arg, param in inspect.signature(getattr(HIP, name)).parameters.items(): + if arg in ('self', 'code', 'param') or 'Enum_' not in str(param.annotation): + continue + found.add(f'{name}.{arg}') + + self.assertEqual(found - EXEMPT, {f'{maker}.{argument}' for maker, argument, *_ in ROWS}) + + def test_no_maker_takes_a_removed_keyword(self) -> None: + """#2594: no maker's signature declares a ``*_default``, ``*_namespace`` or ``*_reversed`` keyword.""" + from pcapkit.protocols.internet.hip import HIP + + for name in vars(HIP): + if name != 'make' and not name.startswith('_make_'): + continue + with self.subTest(maker=name): + removed = [arg for arg in inspect.signature(getattr(HIP, name)).parameters + if arg.endswith(('_default', '_namespace', '_reversed'))] + self.assertEqual(removed, []) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/protocols/internet/test_hip_unit.py b/tests/protocols/internet/test_hip_unit.py index 1924c26e20..3c47685b9c 100644 --- a/tests/protocols/internet/test_hip_unit.py +++ b/tests/protocols/internet/test_hip_unit.py @@ -1349,7 +1349,7 @@ def test_hip_make_param_encrypted_preserves_iv_on_pack(self) -> None: self.assertIn(iv, packed) self.assertEqual(schema.cipher, Cipher.AES_128_CBC) - # a cipher that needs no IV (e.g. ``NULL_ENCRYPT``) still packs + # a cipher that needs no IV (e.g. ``NULL_ENCRYPT``) packs # cleanly, with no IV octets and no "missing HIP_CIPHER" warning # (the resolved cipher is known directly; there is nothing to # infer from a sibling parameter this schema was never given). @@ -1971,7 +1971,7 @@ def test_hip_transport_format_list_parameter_accepts_an_empty_list_at_length_zer Two copies -- a lone one is representable (#651) -- because two consecutive parameters are what prove the reader's *stride*. A parser - that pads by the wrong amount still reads a lone parameter correctly + that pads by the wrong amount reads a lone parameter correctly and only lands in the wrong place at the start of the next one, so a single-parameter fixture cannot tell a correct padding rule from any other. @@ -2334,7 +2334,7 @@ def test_hip_r1_counter_code_128_resolves_to_its_own_schema(self) -> None: 128 and 129 to ``_read_param_r1_counter``, and ``_make_param_r1_counter`` builds a real ``R1CounterParameter`` for 128 -- so construction works either way and only parsing can break. The - *schema* used to unpack a parameter off the wire is chosen by a + *schema* that unpacks a parameter off the wire is chosen by a different mapping, :attr:`~pcapkit.protocols.schema.schema.EnumSchema. registry`, wired into :class:`HIP`'s ``param`` field as ``OptionField(..., registry=Parameter.registry)``, and that one is @@ -2467,7 +2467,7 @@ def test_hip_schema_selectors_and_encrypted_parameter_branches(self) -> None: self.assertEqual(hip_schema.transport_format_list_len({'len': 0}), 0) self.assertEqual(hip_schema.transport_format_list_len({'len': 4}), 4) # unreachable from real wire bytes (``len`` is unsigned on the wire), - # but a direct, bypassing construction call could still pass a + # but a direct, bypassing construction call could pass a # negative ``len``; keep it to the same floor-and-raise discipline. with self.assertRaisesRegex(FieldValueError, 'invalid parameter length'): hip_schema.transport_format_list_len({'len': -1}) @@ -2606,7 +2606,7 @@ def test_hip_solution_parameter_length_is_two_whole_octet_fields(self) -> None: self.assertEqual(reparsed.solution, solution) def test_hip_solution_parameter_at_57_bits_stays_legal_for_hipv1(self) -> None: - """#608: a 57-bit puzzle value must still build the 20-octet + """#608: a 57-bit puzzle value must build the 20-octet ``SOLUTION`` parameter that HIPv1 requires. :rfc:`5201#section-5.2.5` fixes ``Random #I`` and ``Puzzle solution #J`` @@ -2771,7 +2771,7 @@ def test_hip_solution_second_octet_is_reserved_not_a_lifetime(self) -> None: self.assertNotIn('lifetime', hip_data.SolutionParameter.__annotations__) self.assertIn('reserved', hip_schema.SolutionParameter.__annotations__) self.assertNotIn('lifetime', hip_schema.SolutionParameter.__annotations__) - # ... while PUZZLE, where the RFC does put one, still does. + # ... while PUZZLE, where the RFC does put one, has it. self.assertIn('lifetime', hip_data.PuzzleParameter.__annotations__) # Zero when sent, for a from-scratch build, at the default and explicitly. @@ -2819,7 +2819,7 @@ def test_hip_solution_second_octet_is_reserved_not_a_lifetime(self) -> None: self.assertEqual(bytes(rebuilt)[:4 + rebuilt.len], received[:4 + rebuilt.len]) self.assertEqual(bytes(rebuilt), bytes(received_schema)) - # ... but an explicit `reserved` still overrides it, which is the only way to + # ... but an explicit `reserved` overrides it, which is the only way to # write the conformant zero over a peer's non-conformant octet: the data # model is immutable, so the parsed object cannot be corrected in place. with self.assertRaises(UnsupportedCall): @@ -3369,7 +3369,7 @@ def test_hip_parameter_records_are_eight_octet_aligned_on_the_wire(self) -> None is not a corner of the parameter space. The last assertion is the stride check: two consecutive parameters - through the full parser. A wrong padding rule still reads a *lone* + through the full parser. A wrong padding rule reads a *lone* parameter correctly -- it only lands in the wrong place at the start of the next one -- so one record cannot distinguish any padding rule from any other. diff --git a/tests/protocols/internet/test_hip_wire_semantics_unit.py b/tests/protocols/internet/test_hip_wire_semantics_unit.py index b8474a259b..c252bb082b 100644 --- a/tests/protocols/internet/test_hip_wire_semantics_unit.py +++ b/tests/protocols/internet/test_hip_wire_semantics_unit.py @@ -106,8 +106,7 @@ def test_an_identifier_rebuilds_at_the_width_it_was_read_with(self) -> None: self.assertEqual(parsed.id, 1) # type: ignore[attr-defined] self.assertEqual(parsed.id_len, 4) # type: ignore[attr-defined] - # The whole point: byte-exact, where it used to come back as - # ``...000101000000``. + # The whole point: byte-exact, not ``...000101000000``. self.assertEqual(self._params(code, parsed).hex(), raw_hex) def test_an_identifier_width_may_be_declared_or_inferred(self) -> None: @@ -141,7 +140,7 @@ def test_hip_transform_is_refused_outside_version_1(self) -> None: ``_read_param_hip_transform`` all along -- the packet is parsed back on the way out. Going through the public path therefore cannot tell a gated maker from an ungated one, which is the distinction the issue is - about: the maker used to return ``0241000200010000`` for HIPv2, octets + about: an ungated maker returns ``0241000200010000`` for HIPv2, octets its own reader rejects. """ @@ -179,8 +178,8 @@ def test_a_locator_with_an_spi_needs_no_explicit_type(self) -> None: {'locator_set': [{'ip': '2001:db8::1', 'type': 1, 'spi': 7}]}) - # ``spi=`` alone used to raise ``FieldValueError: invalid locator type - # or length``; it now agrees with the form that restates the type. + # ``spi=`` alone agrees with the form that restates the type, rather + # than raising ``FieldValueError: invalid locator type or length``. self.assertEqual(derived.hex(), restated.hex()) # Locator Type 1, Locator Length 5 -- the SPI plus a 128-bit address. self.assertEqual(derived[4:7].hex(), '000105') @@ -251,7 +250,7 @@ def test_a_registration_lifetime_duration_encodes_to_its_octet(self) -> None: """#2281: the makers take a duration and encode it per the RFC.""" from pcapkit.const.hip.parameter import Parameter - # One second is octet 64, not octet 1; the old maker wrote 1. + # One second is octet 64, not octet 1. self.assertEqual( self._params(Parameter.REG_REQUEST, {'lifetime': datetime.timedelta(seconds=1), @@ -313,7 +312,7 @@ def test_overlay_ttl_is_a_hop_count(self) -> None: self.assertEqual(raw.hex(), 'fa0b000400050000') parsed = self._parse(Parameter.OVERLAY_TTL, raw) - # It used to come back as ``timedelta(seconds=5)``. + # An int, not ``timedelta(seconds=5)``. self.assertEqual(parsed.ttl, 5) # type: ignore[attr-defined] self.assertIsInstance(parsed.ttl, int) # type: ignore[attr-defined] self.assertNotIsInstance(parsed.ttl, datetime.timedelta) # type: ignore[attr-defined] @@ -330,8 +329,8 @@ def test_cert_group_is_a_plain_octet(self) -> None: 'cert_id': 1, 'cert': b'x'}) parsed = self._parse(Parameter.CERT, raw) - # ``7`` used to decode as ````, a Diffie-Hellman - # curve name on a field :rfc:`8002#section-2` leaves to the sender. + # ``7`` stays an int, not the curve ````, on a field + # :rfc:`8002#section-2` leaves to the sender. self.assertEqual(parsed.cert_group, 7) # type: ignore[attr-defined] self.assertIs(type(parsed.cert_group), int) # type: ignore[attr-defined] self.assertEqual(self._params(Parameter.CERT, parsed).hex(), raw.hex()) @@ -352,8 +351,9 @@ def test_cert_group_takes_no_enum_lookup_arguments(self) -> None: for dropped in ('cert_group_default', 'cert_group_namespace', 'cert_group_reversed'): self.assertNotIn(dropped, keywords) - # ``cert_type`` keeps its own, since :rfc:`8002` really does register it. - self.assertIn('cert_type_namespace', keywords) + # ``cert_type`` stays an enumeration field, since :rfc:`8002` really + # does register it; it takes the member or its code (#2594). + self.assertIn('Enum_Certificate', str(keywords['cert_type'].annotation)) # -- #2299 ------------------------------------------------------------ diff --git a/tests/protocols/internet/test_icmp_base_dispatch_2574_unit.py b/tests/protocols/internet/test_icmp_base_dispatch_2574_unit.py index dc29c52ef1..3a83906a88 100644 --- a/tests/protocols/internet/test_icmp_base_dispatch_2574_unit.py +++ b/tests/protocols/internet/test_icmp_base_dispatch_2574_unit.py @@ -35,9 +35,10 @@ def pseudo() -> 'object': """An IPv6 pseudo-header, which only ICMPv6 takes.""" + from pcapkit.const.reg.transtype import TransType from pcapkit.protocols.misc.checksum.ip_pseudo import IP_Pseudo - return IP_Pseudo(src='2001:db8::1', dst='2001:db8::2', protocol='IPv6_ICMP', upper_length=0) + return IP_Pseudo(src='2001:db8::1', dst='2001:db8::2', protocol=TransType.IPv6_ICMP, upper_length=0) class ICMPBaseDispatchTests(unittest.TestCase): diff --git a/tests/protocols/internet/test_icmp_checksum_2470_unit.py b/tests/protocols/internet/test_icmp_checksum_2470_unit.py index 581cd807e6..8a3e252441 100644 --- a/tests/protocols/internet/test_icmp_checksum_2470_unit.py +++ b/tests/protocols/internet/test_icmp_checksum_2470_unit.py @@ -59,6 +59,9 @@ class ChecksumTestCase(unittest.TestCase): def setUp(self) -> None: reimport_once_per_class(self) + from pcapkit.const.icmp.type import Type as ICMPv4Type + from pcapkit.const.icmpv6.type import Type as ICMPv6Type + from pcapkit.const.reg.transtype import TransType from pcapkit.corekit.checksum.internet import ChecksumStatus from pcapkit.protocols.internet.icmpv4 import ICMPv4 from pcapkit.protocols.internet.icmpv6 import ICMPv6 @@ -69,6 +72,7 @@ def setUp(self) -> None: from pcapkit.utilities.warnings import ChecksumWarning, ProtocolWarning self.ICMPv4, self.ICMPv6, self.IPv4, self.IPv6, self.IP_Pseudo = ICMPv4, ICMPv6, IPv4, IPv6, IP_Pseudo + self.ICMPv4Type, self.ICMPv6Type, self.TransType = ICMPv4Type, ICMPv6Type, TransType self.Status = ChecksumStatus self.config = config self.ProtocolWarning = ProtocolWarning @@ -135,20 +139,21 @@ def test_parse_rebuild_is_exact(self) -> None: def test_make_computes_the_checksum(self) -> None: body = {'id': 0x3f21, 'seq': 1, 'data': b'odd'} - made = self.ICMPv4(type='Echo', body=body).data + made = self.ICMPv4(type=self.ICMPv4Type.Echo, body=body).data self.assertEqual(made[2:4], expected(b'', made)) icmp, messages = self.parse(made, self.ICMPv4) self.assertEqual((icmp.info.checksum_status, messages), (self.Status.VALID, [])) self.assertEqual(self.ICMPv4.from_data(icmp.info).data, made) with mock.patch.object(self.config, 'CHECKSUM_COMPUTE', False): - self.assertEqual(self.ICMPv4(type='Echo', body=body).data[2:4], b'\x00\x00') - self.assertEqual(self.ICMPv4(type='Echo', body=body, checksum=b'\xbe\xef').data[2:4], b'\xbe\xef') + self.assertEqual(self.ICMPv4(type=self.ICMPv4Type.Echo, body=body).data[2:4], b'\x00\x00') + self.assertEqual(self.ICMPv4(type=self.ICMPv4Type.Echo, body=body, checksum=b'\xbe\xef').data[2:4], + b'\xbe\xef') @unittest.skipUnless(HAS_SCAPY, 'scapy not installed') def test_make_agrees_with_scapy(self) -> None: from scapy.layers.inet import ICMP - made = self.ICMPv4(type='Echo', body={'id': 7, 'seq': 2, 'data': b'ping'}).data + made = self.ICMPv4(type=self.ICMPv4Type.Echo, body={'id': 7, 'seq': 2, 'data': b'ping'}).data packet = ICMP(made) del packet.chksum self.assertEqual(bytes(packet), made) @@ -198,8 +203,10 @@ def test_parse_rebuild_is_exact(self) -> None: def make(self, **kwargs: object) -> bytes: """The octets of an IPv6 packet carrying an Echo Request made with ``kwargs``.""" - icmp = self.ICMPv6(type='Echo_Request', body={'id': 7, 'seq': 2, 'data': b'odd'}, **kwargs) - return self.IPv6(src='2001:db8::1', dst='2001:db8::2', next='IPv6_ICMP', payload=icmp).data + icmp = self.ICMPv6(type=self.ICMPv6Type.Echo_Request, body={'id': 7, 'seq': 2, 'data': b'odd'}, + **kwargs) + return self.IPv6(src='2001:db8::1', dst='2001:db8::2', next=self.TransType.IPv6_ICMP, + payload=icmp).data def test_the_ipv6_layer_computes_it(self) -> None: made = self.make() @@ -209,11 +216,13 @@ def test_the_ipv6_layer_computes_it(self) -> None: self.assertEqual(self.IPv6.from_data(layer.info).data, made) def test_make_takes_the_pseudo_header(self) -> None: - pseudo = self.IP_Pseudo(src='2001:db8::1', dst='2001:db8::2', protocol='IPv6_ICMP', upper_length=0) - message = self.ICMPv6(type='Echo_Request', body={'id': 7, 'seq': 2, 'data': b'odd'}, pseudo=pseudo).data + pseudo = self.IP_Pseudo(src='2001:db8::1', dst='2001:db8::2', protocol=self.TransType.IPv6_ICMP, + upper_length=0) + message = self.ICMPv6(type=self.ICMPv6Type.Echo_Request, body={'id': 7, 'seq': 2, 'data': b'odd'}, + pseudo=pseudo).data self.assertEqual(message, self.make()[40:]) # Alone, with no pseudo-header to compute it over, it is left zero. - self.assertEqual(self.ICMPv6(type='Echo_Request').data[2:4], b'\x00\x00') + self.assertEqual(self.ICMPv6(type=self.ICMPv6Type.Echo_Request).data[2:4], b'\x00\x00') @unittest.skipUnless(HAS_SCAPY, 'scapy not installed') def test_make_agrees_with_scapy(self) -> None: diff --git a/tests/protocols/internet/test_igmp_checksum_2618_unit.py b/tests/protocols/internet/test_igmp_checksum_2618_unit.py index c012be49dd..5de12e0622 100644 --- a/tests/protocols/internet/test_igmp_checksum_2618_unit.py +++ b/tests/protocols/internet/test_igmp_checksum_2618_unit.py @@ -218,8 +218,10 @@ def test_the_computed_checksum_covers_the_payload(self) -> None: self.assertEqual(made, message(packet[:2], packet[4:] + b'XY')) def test_the_ipv4_payload_verifies(self) -> None: + from pcapkit.const.reg.transtype import TransType + igmp = self.classes['IGMPv2'](**self.FIELDS['v2-report']) - made = self.IPv4(src='10.0.0.1', dst='239.1.2.3', protocol='IGMP', payload=igmp).data + made = self.IPv4(src='10.0.0.1', dst='239.1.2.3', protocol=TransType.IGMP, payload=igmp).data self.assertEqual(made[20:], MESSAGES['v2-report'][0]) self.assertEqual(self.status(made), (self.Status.VALID, [])) diff --git a/tests/protocols/internet/test_igmp_unit.py b/tests/protocols/internet/test_igmp_unit.py index 5f7ac96aff..c9c84b6b0a 100644 --- a/tests/protocols/internet/test_igmp_unit.py +++ b/tests/protocols/internet/test_igmp_unit.py @@ -743,7 +743,7 @@ def test_max_resp_time_bounds_round_trip(self) -> None: class IGMPv3MaxRespCodeTests(unittest.TestCase): """The floating-point Max Resp Code/QQIC decode (:rfc:`3376#section-4.1.1`, - :rfc:`3376#section-4.1.7`), now module-level helpers on + :rfc:`3376#section-4.1.7`), module-level helpers on :mod:`pcapkit.protocols.internet.igmpv3`. :rfc:`3376#section-4.1.1` gives the formula as:: @@ -1513,7 +1513,8 @@ def test_version_picks_the_class_from_fields(self) -> None: 'v1 defaults': ({}, 'IGMPv1'), 'v1 report': ({'type': Type.IGMPv1_Membership_Report, 'group': '224.0.0.1'}, 'IGMPv1'), 'v2 query': ({'type': Type.IGMP_Membership_Query, 'max_resp_time': 100}, 'IGMPv2'), - 'v2 leave by name': ({'type': 'IGMPv2_Leave_Group', 'group': '239.1.2.3'}, 'IGMPv2'), + 'v2 leave by name': ({'type': IGMP.resolve_index('IGMPv2_Leave_Group', namespace=Type), + 'group': '239.1.2.3'}, 'IGMPv2'), 'v3 report by type': ({'type': Type.IGMPv3_Membership_Report}, 'IGMPv3'), 'v3 query by field': ({'type': Type.IGMP_Membership_Query, 'qrv': 2, 'qqic': 125}, 'IGMPv3'), } diff --git a/tests/protocols/internet/test_internet_maker_ranges_2614_unit.py b/tests/protocols/internet/test_internet_maker_ranges_2614_unit.py index ed3e135f7f..81be203558 100644 --- a/tests/protocols/internet/test_internet_maker_ranges_2614_unit.py +++ b/tests/protocols/internet/test_internet_maker_ranges_2614_unit.py @@ -26,10 +26,15 @@ """ +import enum import inspect import io +import re +import sys import unittest +import aenum + from tests._support import reimport_once_per_class #: Annotation fragments that mark a keyword argument as something other than a @@ -37,13 +42,26 @@ NOT_AN_INTEGER = ('Enum', 'Address', 'bytes', 'str') +def _takes_enumeration(func: 'object', param: 'inspect.Parameter') -> 'bool': + """Whether ``param`` of ``func`` takes a registry code. + + Its annotation's head, past an ``Optional[``, names an enumeration in the module + defining ``func``, as ``Enum_ToSPrecedence | int`` does (#2594). + + """ + head = re.match(r'(?:Optional\[)?(\w+)', str(param.annotation)) + module = vars(sys.modules[func.__module__]) # type: ignore[attr-defined] + kind = module.get(head.group(1)) if head is not None else None + return isinstance(kind, type) and issubclass(kind, (enum.Enum, aenum.Enum)) + + def integer_arguments(func: 'object') -> 'list[str]': """Keyword arguments of ``func`` that take a plain integer.""" names = [] for param in inspect.signature(func).parameters.values(): # type: ignore[arg-type] if param.kind not in (param.POSITIONAL_OR_KEYWORD, param.KEYWORD_ONLY): continue - if param.name.endswith(('_default', '_reversed', '_namespace')): + if _takes_enumeration(func, param): continue ann = str(param.annotation) default = param.default @@ -57,9 +75,9 @@ def integer_arguments(func: 'object') -> 'list[str]': def registry_arguments(func: 'object') -> 'list[str]': - """Keyword arguments of ``func`` that take a registry code: those with a ``_namespace``.""" - params = inspect.signature(func).parameters # type: ignore[arg-type] - return [name for name in params if f'{name}_namespace' in params] + """Keyword-only arguments of ``func`` that take a registry code.""" + return [param.name for param in inspect.signature(func).parameters.values() # type: ignore[arg-type] + if param.kind is param.KEYWORD_ONLY and _takes_enumeration(func, param)] def octet_arguments(func: 'object') -> 'list[str]': @@ -972,7 +990,8 @@ class OptionCodeTests(_Case): """The option makers refuse a registry code their field cannot carry.""" #: One-bit codes, which take a :obj:`bool` as ``_make_uint`` lets one-bit fields. - ONE_BIT = {'MH._make_opt_ipv4_dhcp.mode'} + ONE_BIT = {'HOPOPT._make_opt_smf_dpd.mode', 'IPv6_Opts._make_opt_smf_dpd.mode', + 'MH._make_opt_ipv4_dhcp.mode'} def _builders(self) -> 'list[tuple[type, object]]': from pcapkit.const.mh.packet import Packet diff --git a/tests/protocols/internet/test_ip_make_codes_2594_unit.py b/tests/protocols/internet/test_ip_make_codes_2594_unit.py new file mode 100644 index 0000000000..fa0198d07d --- /dev/null +++ b/tests/protocols/internet/test_ip_make_codes_2594_unit.py @@ -0,0 +1,246 @@ +# -*- coding: utf-8 -*- +"""The IPv4, ICMP, IGMP and IPX makers take a member or a code, never a name. + +GitHub issue #2594: each enumeration field of these makers takes a member of +its enumeration or an :class:`int` code, resolved with the enumeration's +``get``, and refuses with a :exc:`~pcapkit.utilities.exceptions.ProtocolError` +a member's name, a :obj:`bool` for a field wider than one bit (#2617), and a +code the field cannot carry. Unchecked, a name resolves against an enumeration +inferred from the caller's frame, ``IPv4(protocol=256)`` writes ``0x00``, a +too-wide ToS code raises a bare +:exc:`~pcapkit.utilities.exceptions.FieldValueError`, and every ToS field takes +``True``. A one-bit field takes ``True`` -- the IPv4 delay, throughput and +reliability bits of :rfc:`791` -- and writes it as ``1``. + +``ROWS`` holds one row per migrated field: the maker, the field, its +enumeration, its width in bits, a member to build with, and where the code +lands in the octets. An option maker is reached through +:meth:`IPv4.make `, and the group +record maker through :meth:`IGMPv3.make +`, as a caller reaches them. + +The type of each IPv4 option is such a field as well, given to ``make`` in a +``(type, kwargs)`` pair or as the key of parsed options: :class:`OptionTypeTests` +refuses ``1 << 8``, ``-1``, ``True`` and a member's name there with the exact +message, and builds, parses back and rebuilds the widest type, ``255``. + +Every case builds its own octets in memory and reads no capture. Classes are +imported inside each test, after :func:`~tests._support.reimport_once_per_class`. + +""" +# Deferring these is the design: each build runs after the per-class re-import. +# pylint: disable=import-outside-toplevel + +import importlib +import io +import unittest + +from tests._support import reimport_once_per_class + +#: Option numbers of the IPv4 options under test (RFC 791, RFC 2113, RFC 4782). +SEC, RTRALT, QS = 130, 148, 25 + + +def _ipv4(**kwargs: 'object') -> 'bytes': + from pcapkit.protocols.internet.ipv4 import IPv4 + + return IPv4(**kwargs).data + + +def _ipv4_option(code: 'int', field: 'str') -> 'object': + def build(value: 'object') -> 'bytes': + from pcapkit.const.ipv4.option_number import OptionNumber + + return _ipv4(options=[(OptionNumber.get(code), {field: value})]) + return build + + +def _ipx(value: 'object') -> 'bytes': + from pcapkit.protocols.internet.ipx import IPX + + return IPX(type=value).data + + +def _icmpv4(value: 'object') -> 'bytes': + from pcapkit.protocols.internet.icmpv4 import ICMPv4 + + return ICMPv4(type=value).data + + +def _icmpv6(value: 'object') -> 'bytes': + from pcapkit.protocols.internet.icmpv6 import ICMPv6 + + return ICMPv6(type=value).data + + +def _igmpv2(value: 'object') -> 'bytes': + # IGMPv2 makes with the family base's IGMP.make. + from pcapkit.protocols.internet.igmpv2 import IGMPv2 + + return IGMPv2(type=value).data + + +def _igmpv3(value: 'object') -> 'bytes': + from pcapkit.protocols.internet.igmpv3 import IGMPv3 + + return IGMPv3(type=value).data + + +def _igmpv3_record(value: 'object') -> 'bytes': + from pcapkit.const.igmp.type import Type + from pcapkit.protocols.internet.igmpv3 import IGMPv3 + + return IGMPv3(type=Type.IGMPv3_Membership_Report, records=[{'record_type': value}]).data + + +#: ``(label, build, enumeration, member, bits, read)``: ``build(value)`` makes +#: octets with ``value`` in the field; ``enumeration`` is ``module:class``; +#: ``member`` is a member's name, chosen off the field's default; ``bits`` is +#: the field's width; ``read(octets)`` is the code the octets carry there. +ROWS = ( + # IPv4 header, RFC 791 section 3.1: the ToS octet 1 (ECN, RFC 3168 + # section 5, in its two low-order bits) and Protocol octet 9. + ('IPv4.make tos_pre', lambda v: _ipv4(tos_pre=v), 'pcapkit.const.ipv4.tos_pre:ToSPrecedence', + 'Network_Control', 3, lambda o: o[1] >> 5), + ('IPv4.make tos_del', lambda v: _ipv4(tos_del=v), 'pcapkit.const.ipv4.tos_del:ToSDelay', + 'LOW', 1, lambda o: o[1] >> 4 & 1), + ('IPv4.make tos_thr', lambda v: _ipv4(tos_thr=v), 'pcapkit.const.ipv4.tos_thr:ToSThroughput', + 'HIGH', 1, lambda o: o[1] >> 3 & 1), + ('IPv4.make tos_rel', lambda v: _ipv4(tos_rel=v), 'pcapkit.const.ipv4.tos_rel:ToSReliability', + 'HIGH', 1, lambda o: o[1] >> 2 & 1), + ('IPv4.make tos_ecn', lambda v: _ipv4(tos_ecn=v), 'pcapkit.const.ipv4.tos_ecn:ToSECN', + 'CE', 2, lambda o: o[1] & 0b11), + ('IPv4.make protocol', lambda v: _ipv4(protocol=v), 'pcapkit.const.reg.transtype:TransType', + 'TCP', 8, lambda o: o[9]), + # The option follows the 20-octet header: type, length, then the field. + ('IPv4 SEC level', _ipv4_option(SEC, 'level'), + 'pcapkit.const.ipv4.classification_level:ClassificationLevel', 'Top_Secret', 8, lambda o: o[22]), + ('IPv4 RTRALT alert', _ipv4_option(RTRALT, 'alert'), 'pcapkit.const.ipv4.router_alert:RouterAlert', + 'Reserved_65535', 16, lambda o: int.from_bytes(o[22:24], 'big')), + ('IPv4 QS func', _ipv4_option(QS, 'func'), 'pcapkit.const.ipv4.qs_function:QSFunction', + 'Report_of_Approved_Rate', 4, lambda o: o[22] >> 4), + # IPX Packet Type follows Checksum, Packet Length and Transport Control. + ('IPX.make type', _ipx, 'pcapkit.const.ipx.packet:Packet', 'NCP', 8, lambda o: o[5]), + ('ICMPv4.make type', _icmpv4, 'pcapkit.const.icmp.type:Type', 'Destination_Unreachable', 8, + lambda o: o[0]), + ('ICMPv6.make type', _icmpv6, 'pcapkit.const.icmpv6.type:Type', 'Echo_Reply', 8, lambda o: o[0]), + ('IGMP.make type', _igmpv2, 'pcapkit.const.igmp.type:Type', 'IGMPv2_Leave_Group', 8, lambda o: o[0]), + ('IGMPv3.make type', _igmpv3, 'pcapkit.const.igmp.type:Type', 'IGMPv3_Membership_Report', 8, + lambda o: o[0]), + # RFC 3376 section 4.2: the first group record follows the 8-octet header. + ('IGMPv3 group record record_type', _igmpv3_record, 'pcapkit.const.igmp.record_type:RecordType', + 'BLOCK_OLD_SOURCES', 8, lambda o: o[8]), +) + + +def member(row: 'tuple') -> 'object': + """The member a row builds with.""" + module, name = row[2].split(':') + return getattr(getattr(importlib.import_module(module), name), row[3]) + + +class MakeCodeTests(unittest.TestCase): + """Each row's field takes the member or its code, and refuses anything else.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def assertRefused(self, build: 'object', value: 'object') -> None: + """``build(value)`` raises a ProtocolError, rather than building or raising another error.""" + from pcapkit.utilities.exceptions import ProtocolError + + try: + octets = build(value) # type: ignore[operator] + except ProtocolError: + return + except Exception as exc: # pylint: disable=broad-except + self.fail(f'{value!r} raised {type(exc).__name__}, not ProtocolError: {exc}') + self.fail(f'{value!r} was accepted, building {octets.hex()}') + + def test_a_code_too_wide_is_refused(self) -> None: + for row in ROWS: + with self.subTest(row=row[0], value=1 << row[4]): + self.assertRefused(row[1], 1 << row[4]) + + def test_a_bool_is_refused_for_a_field_wider_than_one_bit(self) -> None: + for row in ROWS: + if row[4] == 1: + continue + with self.subTest(row=row[0]): + self.assertRefused(row[1], True) + + def test_a_bool_is_one_for_a_one_bit_field(self) -> None: + one_bit = [row for row in ROWS if row[4] == 1] + self.assertEqual([row[0] for row in one_bit], + ['IPv4.make tos_del', 'IPv4.make tos_thr', 'IPv4.make tos_rel']) + for row in one_bit: + with self.subTest(row=row[0]): + self.assertEqual(row[1](True), row[1](1)) + self.assertEqual(row[5](row[1](True)), 1) + + def test_a_name_is_refused(self) -> None: + for row in ROWS: + with self.subTest(row=row[0], name=row[3]): + self.assertRefused(row[1], row[3]) + + def test_a_member_and_its_code_build_the_same_octets(self) -> None: + for row in ROWS: + code = member(row) + with self.subTest(row=row[0], member=code): + octets = row[1](code) + self.assertEqual(octets, row[1](int(code))) # type: ignore[call-overload] + self.assertEqual(row[5](octets), int(code)) # type: ignore[call-overload] + + +class OptionTypeTests(unittest.TestCase): + """The type of an IPv4 option takes a member or a code it can carry, nothing else.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def _builders(self) -> 'dict[str, object]': + """A builder for each form ``options`` takes a type in.""" + from pcapkit.corekit.multidict import OrderedMultiDict + from pcapkit.protocols.internet.ipv4 import IPv4 + + def pair(value: 'object') -> 'bytes': + return _ipv4(options=[(value, {'data': b'\xa5\xa5'})]) + + octets = pair(255) + options = IPv4(io.BytesIO(octets), len(octets)).info.options + _, item = list(options.items(multi=True))[0] + return {'pair': pair, 'parsed': lambda value: _ipv4(options=OrderedMultiDict([(value, item)]))} + + def test_a_type_the_field_cannot_carry_is_refused(self) -> None: + # #2594: checked as an 8-bit field before the EOOL and NOP skip reads it. + from pcapkit.utilities.exceptions import ProtocolError + + for form, build in self._builders().items(): + for value in (1 << 8, -1, True, 'EOOL'): + with self.subTest(form=form, value=value): + try: + octets = build(value) # type: ignore[operator] + except ProtocolError as error: + self.assertEqual(str(error), f'IPv4: invalid option type: {value!r}') + except Exception as error: # pylint: disable=broad-except + self.fail(f'{value!r} raised {type(error).__name__}: {error}') + else: + self.fail(f'{value!r} was accepted, building {octets.hex()}') + + def test_the_widest_type_builds_parses_back_and_rebuilds(self) -> None: + from pcapkit.const.ipv4.option_number import OptionNumber + from pcapkit.protocols.internet.ipv4 import IPv4 + + for form, build in self._builders().items(): + for value in (255, OptionNumber.get(255)): + with self.subTest(form=form, value=value): + octets = build(value) # type: ignore[operator] + # the type octet follows the 20-octet header + self.assertEqual(octets[20], 255) + info = IPv4(io.BytesIO(octets), len(octets)).info + self.assertEqual([int(key) for key in info.options][0], 255) + self.assertEqual(IPv4.from_data(info).data.hex(), octets.hex()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/protocols/internet/test_ipv4_checksum_2472_unit.py b/tests/protocols/internet/test_ipv4_checksum_2472_unit.py index 678ab8ae58..4bbb992bbb 100644 --- a/tests/protocols/internet/test_ipv4_checksum_2472_unit.py +++ b/tests/protocols/internet/test_ipv4_checksum_2472_unit.py @@ -107,9 +107,11 @@ def test_parse_rebuild_is_exact(self) -> None: def make(self, compute: bool = True, **kwargs: object) -> bytes: """The octets of an IPv4 header made with ``kwargs``.""" + from pcapkit.const.reg.transtype import TransType + with mock.patch.object(self.config, 'CHECKSUM_COMPUTE', compute): return self.IPv4(src='192.0.2.1', dst='198.51.100.2', ttl=64, id=0x1234, - protocol='UDP', **kwargs).data + protocol=TransType.UDP, **kwargs).data def test_make_computes_the_checksum(self) -> None: from pcapkit.const.ipv4.option_number import OptionNumber diff --git a/tests/protocols/internet/test_ipv4_later_fragment_raw_unit.py b/tests/protocols/internet/test_ipv4_later_fragment_raw_unit.py index 827a031505..566e32df2e 100644 --- a/tests/protocols/internet/test_ipv4_later_fragment_raw_unit.py +++ b/tests/protocols/internet/test_ipv4_later_fragment_raw_unit.py @@ -114,11 +114,12 @@ def test_first_and_unfragmented_datagrams_are_dissected(self) -> None: def test_issue_repro_later_fragment_in_a_frame(self) -> None: # the issue's reproduction, carried in an Ethernet frame as a capture # would carry it: ``'UDP' in frame`` does not hold + from pcapkit.const.reg.transtype import TransType from pcapkit.protocols.internet.ipv4 import IPv4 from pcapkit.protocols.link.ethernet import Ethernet from pcapkit.protocols.misc.raw import Raw - made = IPv4(protocol='UDP', offset=185, payload=Raw(bytes(range(40))), + made = IPv4(protocol=TransType.UDP, offset=185, payload=Raw(bytes(range(40))), src='1.1.1.1', dst='2.2.2.2') ipv4 = self._parse(made.data) self.assertEqual(ipv4.info.offset, 1480) @@ -137,6 +138,7 @@ def test_made_later_fragment_parses_as_raw(self) -> None: # make -> parse: a later fragment built with a UDP object as its payload # writes the UDP octets, reads back as raw (as IPv6 does, #1545), # and rebuilds to the octets made + from pcapkit.const.reg.transtype import TransType from pcapkit.protocols.internet.ipv4 import IPv4 from pcapkit.protocols.misc.raw import Raw from pcapkit.protocols.transport.udp import UDP @@ -145,7 +147,7 @@ def test_made_later_fragment_parses_as_raw(self) -> None: for options in (None, [bytes((1, 1, 1, 0))]): with self.subTest(options=options): kwargs = {} if options is None else {'options': options} - made = IPv4(protocol='UDP', offset=185, mf=True, payload=upper, + made = IPv4(protocol=TransType.UDP, offset=185, mf=True, payload=upper, src='1.1.1.1', dst='2.2.2.2', **kwargs) self.assertEqual(made.data[made.info.hdr_len:], upper.data) self.assertNotIn('UDP', made) diff --git a/tests/protocols/internet/test_ipv4_tso_total_length_1547_runtime.py b/tests/protocols/internet/test_ipv4_tso_total_length_1547_runtime.py index 6774311fcc..d875df0d51 100644 --- a/tests/protocols/internet/test_ipv4_tso_total_length_1547_runtime.py +++ b/tests/protocols/internet/test_ipv4_tso_total_length_1547_runtime.py @@ -142,11 +142,12 @@ def test_tcp_is_parsed_and_every_rebuild_is_byte_exact(self) -> None: self.assertEqual((first.srcport.port, first.dstport.port), (22, 53406)) def test_make_with_a_zero_total_length_round_trips(self) -> None: + from pcapkit.const.reg.transtype import TransType from pcapkit.protocols.internet.ipv4 import IPv4 from pcapkit.protocols.transport.tcp import TCP segment = _records()[1][0][3][34:] - made = IPv4(total_length=0, protocol='TCP', ttl=64, src='10.20.30.130', dst='10.20.30.131', + made = IPv4(total_length=0, protocol=TransType.TCP, ttl=64, src='10.20.30.130', dst='10.20.30.131', payload=segment) self.assertEqual(made.data[2:4], b'\x00\x00') self.assertEqual(made.data[20:], segment) @@ -158,7 +159,7 @@ def test_make_with_a_zero_total_length_round_trips(self) -> None: self.assertEqual(IPv4.from_data(parsed.info.to_dict()).data, made.data) # Without an explicit Total Length it is computed. - computed = IPv4(protocol='TCP', payload=segment) + computed = IPv4(protocol=TransType.TCP, payload=segment) self.assertEqual(struct.unpack('!H', computed.data[2:4])[0], 20 + len(segment)) def test_schema_splits_payload_and_trailer(self) -> None: @@ -182,6 +183,7 @@ def test_a_bogus_total_length_is_not_dissected_further(self) -> None: # stops at ``eth:ethertype:ip``. So no payload layer is parsed from it, # no negative length reaches one, the octets after the header are kept # as the trailer, and the Total Length is written back as it was. + from pcapkit.const.reg.transtype import TransType from pcapkit.protocols.internet.ipv4 import IPv4 from pcapkit.protocols.link.ethernet import Ethernet from pcapkit.protocols.misc.no_payload import NoPayload @@ -215,7 +217,7 @@ def spy(self: 'Any', dict_: 'Any', proto: 'Any', length: 'Any' = None, **kwargs: self.assertEqual(IPv4.from_data(ipv4.info.to_dict()).data, octets[14:]) # make -> parse -> rebuild - made = IPv4(total_length=10, protocol='TCP', trailer=frame[34:]) + made = IPv4(total_length=10, protocol=TransType.TCP, trailer=frame[34:]) self.assertEqual(made.data[2:4], b'\x00\x0a') parsed_made = IPv4(made.data, len(made.data)) self.assertIsInstance(parsed_made.payload, NoPayload) diff --git a/tests/protocols/internet/test_ipv6_ext_make_codes_2594_unit.py b/tests/protocols/internet/test_ipv6_ext_make_codes_2594_unit.py new file mode 100644 index 0000000000..78d09da709 --- /dev/null +++ b/tests/protocols/internet/test_ipv6_ext_make_codes_2594_unit.py @@ -0,0 +1,232 @@ +# -*- coding: utf-8 -*- +"""The IPv6, IPv6 extension header, AH and ESP makers take an enumeration +field as a member or an :class:`int` code, and refuse what the field cannot carry. + +GitHub issue #2594: each enumeration field of these makers is resolved with the +enumeration's ``get`` through +:meth:`~pcapkit.protocols.protocol.Protocol._make_index`, rather than through a +``X_default`` / ``X_namespace`` / ``X_reversed`` group and an enumeration +inferred from the caller's frame. A name, a :obj:`bool` for a field wider than +one bit, and a code too wide for the field each raise +:exc:`~pcapkit.utilities.exceptions.ProtocolError` naming the field (#2617). +Unchecked, ``IPv6_Route(next=256)`` and ``IPv6_Route(type=256)`` wrote ``0x00`` +(#2676). + +:data:`HEADER_ROWS` and :data:`OPTION_ROWS` hold one row per field. The option +makers' fields are reached through ``make(options=...)``, as a caller reaches +them, in both HOPOPT and IPv6-Opts, which share their code. Each field's width is the one its schema +declares and its RFC gives. ``test_every_enumeration_argument_has_a_row`` finds +the enumeration arguments from the maker signatures, so every one of them needs a +row and every row needs one. + +Every case builds its own octets in memory and reads no capture. The protocol +classes are imported inside each test, after +:func:`~tests._support.reimport_once_per_class`. + +""" + +import inspect +import unittest + +from tests._support import reimport_once_per_class + +#: ``(class, argument, enumeration module, member, bits, field name)``: one row +#: per enumeration field. ``options`` stands for the type of an option given to +#: ``make``. HOPOPT and IPv6-Opts share every option row, see :data:`OPTION_ROWS`. +HEADER_ROWS = ( + # RFC 8200 sections 4.3 to 4.6: Next Header is 8 bits in each extension header. + ('HOPOPT', 'next', 'reg.transtype', 'TransType.IPv6_NoNxt', 8, 'next header'), + ('IPv6_Opts', 'next', 'reg.transtype', 'TransType.IPv6_NoNxt', 8, 'next header'), + ('IPv6_Route', 'next', 'reg.transtype', 'TransType.IPv6_NoNxt', 8, 'next header'), + # RFC 8200 section 4.4: Routing Type is 8 bits. + ('IPv6_Route', 'type', 'ipv6.routing', 'Routing.Nimrod', 8, 'routing type'), + ('IPv6_Frag', 'next', 'reg.transtype', 'TransType.IPv6_NoNxt', 8, 'next header'), + # RFC 8200 section 3: Next Header is 8 bits. + ('IPv6', 'next', 'reg.transtype', 'TransType.IPv6_NoNxt', 8, 'next header'), + # RFC 6564 section 4: the generic header's Next Header is 8 bits. + ('IPv6_Ext', 'next', 'reg.transtype', 'TransType.IPv6_NoNxt', 8, 'next header'), + # RFC 4302 section 2.1 and RFC 4303 section 2.6: Next Header is 8 bits. + ('AH', 'next', 'reg.transtype', 'TransType.IPv6_NoNxt', 8, 'next header'), + ('ESP', 'next', 'reg.transtype', 'TransType.IPv6_NoNxt', 8, 'next header'), +) + +#: ``(option, argument, enumeration module, member, bits, field name, other +#: arguments, data attribute)`` for the option makers of HOPOPT and IPv6-Opts. +#: The data attribute is where the parsed option keeps the field. +OPTION_ROWS = ( + # RFC 8200 section 4.2: Option Type is 8 bits. + (None, 'options', 'ipv6.option', 'Option.Router_Alert', 8, 'option type', {}, None), + # RFC 2711 section 2.1: Value is 2 octets. + ('Router_Alert', 'alert', 'ipv6.router_alert', 'RouterAlert.Aggregated_Reservation_Nesting_Level_1', + 16, 'router alert value', {}, 'value'), + # RFC 6621 section 6.1.1: the H bit is 1 bit, TidTy 3 bits. + ('SMF_DPD', 'mode', 'ipv6.smf_dpd_mode', 'SMFDPDMode.H_DPD', 1, 'DPD mode', + {'hav': b'\x01', 'id': b'\xaa'}, 'dpd_type'), + ('SMF_DPD', 'tid_type', 'ipv6.tagger_id', 'TaggerID.DEFAULT', 3, 'TaggerID type', + {'tid': b'\x0a\x0b\x0c\x0d', 'id': b'\xaa\xbb'}, 'tid_type'), + # RFC 4782 section 3.1: Function is 4 bits. + ('Quick_Start', 'func', 'ipv6.qs_function', 'QSFunction.Report_of_Approved_Rate', 4, + 'QS function', {}, 'func'), + # RFC 7731 section 6.1: S is 2 bits. + ('MPL_Option', 'seed_type', 'ipv6.seed_id', 'SeedID.SEEDID_64_BIT_UNSIGNED_INTEGER', 2, + 'MPL seed-id type', {'seed': 5}, 'seed_type'), +) + +#: ``{class: (module, alias)}`` of the makers under test. +CLASSES = { + 'HOPOPT': ('hopopt', 'HOPOPT'), + 'IPv6_Opts': ('ipv6_opts', 'IPv6-Opts'), + 'IPv6_Route': ('ipv6_route', 'IPv6-Route'), + 'IPv6_Frag': ('ipv6_frag', 'IPv6-Frag'), + 'IPv6': ('ipv6', 'IPv6'), + 'IPv6_Ext': ('ipv6_ext', 'IPv6-Ext'), + 'AH': ('ah', 'AH'), + 'ESP': ('esp', 'ESP'), +} + + +def _class(name: 'str') -> 'type': + module = __import__(f'pcapkit.protocols.internet.{CLASSES[name][0]}', fromlist=[name]) + return getattr(module, name) + + +def _member(module: 'str', member: 'str') -> 'object': + enum_name, attr = member.split('.') + return getattr(getattr(__import__(f'pcapkit.const.{module}', fromlist=[enum_name]), enum_name), attr) + + +def _esp_context() -> 'object': + from pcapkit.corekit.context import ContextRegistry + from pcapkit.protocols.internet.esp import ESPContext, SecurityAssociation + + # NOTE: An ENCR_NULL association with no integrity algorithm, so that + # ``encrypt=True`` writes ``next`` into the trailer without the cryptography package. + return ContextRegistry.make(ESPContext(SecurityAssociation(spi=1))) + + +class _Row: + """One enumeration field: how to build it, and how to read it back.""" + + def __init__(self, cls: 'str', argument: 'str', module: 'str', member: 'str', bits: 'int', + field: 'str', option: 'object' = None, extra: 'object' = None, + attribute: 'object' = None) -> 'None': + self.cls = cls + self.argument = argument + self.member = _member(module, member) + self.bits = bits + self.field = field + self.option = option + self.extra = extra or {} + self.attribute = attribute + + @property + def label(self) -> 'str': + return f'{self.cls}.{self.argument}' + + @property + def message(self) -> 'str': + return f'{CLASSES[self.cls][1]}: invalid {self.field}' + + def build(self, value: 'object') -> 'bytes': + from pcapkit.const.ipv6.option import Option + + cls = _class(self.cls) + if self.argument == 'options': + return cls(next=59, options=[(value, {})]).data + if self.option is not None: + args = {**self.extra, self.argument: value} + return cls(next=59, options=[(Option[self.option], args)]).data + if self.cls == 'ESP': + return cls(spi=1, seq=1, next=value, encrypt=True, payload=b'abcd', + __context__=_esp_context()).data + if self.cls == 'IPv6_Ext': + return cls(next=value, len=0, data=bytes(6)).data + return cls(**{self.argument: value}).data + + def read(self, octets: 'bytes') -> 'object': + from pcapkit.const.ipv6.extension_header import ExtensionHeader + from pcapkit.const.ipv6.option import Option + + cls = _class(self.cls) + if self.cls == 'IPv6': + return cls(octets, len(octets)).info.next + if self.cls == 'ESP': + return cls(octets, len(octets), __context__=_esp_context()).info.next + if self.cls == 'IPv6_Ext': + return cls(octets, len(octets), extension=True, alias=ExtensionHeader.Shim6).info.next + info = cls(octets, len(octets), extension=True).info + if self.argument == 'options': + return list(info.options.keys())[0] + if self.option is not None: + return getattr(info.options[Option[self.option]], self.attribute) + return getattr(info, self.argument) + + +def _rows() -> 'list[_Row]': + rows = [_Row(*row) for row in HEADER_ROWS] + for cls in ('HOPOPT', 'IPv6_Opts'): + for option, argument, module, member, bits, field, extra, attribute in OPTION_ROWS: + rows.append(_Row(cls, argument, module, member, bits, field, option, extra, attribute)) + return rows + + +class TestEnumerationFieldCodes(unittest.TestCase): + """Each enumeration field takes a member or a code, and refuses the rest.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def assertRefused(self, row: '_Row', value: 'object') -> None: + from pcapkit.utilities.exceptions import ProtocolError + + with self.assertRaises(ProtocolError) as caught: + row.build(value) + self.assertEqual(str(caught.exception), f'{row.message}: {value!r}') + + def test_code_too_wide_for_its_field_is_refused(self) -> None: + for row in _rows(): + with self.subTest(row=row.label, bits=row.bits): + self.assertRefused(row, 1 << row.bits) + + def test_bool_is_refused_where_the_field_is_wider_than_one_bit(self) -> None: + for row in _rows(): + if row.bits == 1: + continue + with self.subTest(row=row.label): + self.assertRefused(row, True) + + def test_member_name_is_refused(self) -> None: + for row in _rows(): + with self.subTest(row=row.label): + self.assertRefused(row, row.member.name) + + def test_member_and_code_build_identical_octets(self) -> None: + for row in _rows(): + with self.subTest(row=row.label): + octets = row.build(row.member) + self.assertEqual(row.build(int(row.member)), octets) + self.assertEqual(row.read(octets), row.member) + + def test_every_enumeration_argument_has_a_row(self) -> None: + # The arguments are found from the signatures: every argument of + # ``make`` and every keyword-only argument of an option maker whose + # annotation names an enumeration. A row naming no such argument fails too. + rows = {row.label for row in _rows()} + swept = set() + for name in CLASSES: + cls = _class(name) + makers = [(cls.make, False)] + for meth in getattr(cls, '__option__', {}).values(): + if isinstance(meth, str) and hasattr(cls, f'_make_opt_{meth}'): + makers.append((getattr(cls, f'_make_opt_{meth}'), True)) + for func, keyword_only in makers: + for param in inspect.signature(func).parameters.values(): + if keyword_only and param.kind is not param.KEYWORD_ONLY: + continue + if 'Enum_' in str(param.annotation): + swept.add(f'{name}.{param.name}') + self.assertEqual(sorted(swept), sorted(rows)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/protocols/internet/test_mh_make_codes_2594_unit.py b/tests/protocols/internet/test_mh_make_codes_2594_unit.py new file mode 100644 index 0000000000..59460c6f41 --- /dev/null +++ b/tests/protocols/internet/test_mh_make_codes_2594_unit.py @@ -0,0 +1,461 @@ +# -*- coding: utf-8 -*- +"""MH's makers take an enumeration field as a member or an ``int`` code. + +GitHub issue #2594: each enumeration field of ``MH.make`` and of its message, +option and sub-option makers is resolved by ``Protocol._make_index`` with the +enumeration's ``get``. A name, a :obj:`bool` for a field wider than one bit, +and a code the field cannot carry raise ``ProtocolError`` naming the field +(#2617), and a member builds the same octets as its ``int`` code. A caller +holding a name resolves it with ``Protocol.resolve_index`` first. The widest +code each field carries builds, parses back and rebuilds as captured, as does +the parsed value given back to ``make``, also where no member holds it and the +enumeration's own range is narrower than the field. + +The type of each option, sub-option, QoS attribute and CGA extension is such a +field too, given in a list of ``(type, kwargs)`` pairs or as the key of parsed +data: :data:`LIST_ROWS` holds one row per list maker, and a negative code joins +the refused values there. + +Each row reaches its field the way a caller does: through ``MH.make``, with the +message as ``data`` and an option, sub-option or request in its list. Every case +builds its own octets in memory and reads no capture; the protocol classes are +imported inside each test, after :func:`~tests._support.reimport_once_per_class`. + +""" +# Deferring these is the design: each build runs after the per-class re-import. +# pylint: disable=import-outside-toplevel + +import importlib +import inspect +import io +import unittest +from typing import TYPE_CHECKING + +from tests._support import reimport_once_per_class + +if TYPE_CHECKING: + from typing import Any + +#: ``(label, kind, target, field, enumeration, member, bits, name, base)``, one +#: row per enumeration field. ``kind`` says how ``target`` is reached: the +#: header of ``make``, a message by its ``Packet`` name, an option by its +#: ``Option`` name, a Context Request entry, or a sub-option given as ``(option, +#: list argument, sub-option enumeration, sub-option)``. ``enumeration`` is +#: ``module:class``, ``bits`` the field's width in the schema, ``name`` the +#: field's name in the error message, and ``base`` the other arguments of the +#: message, option or sub-option. +ROWS = ( + ('make.next', 'header', None, 'next', + 'pcapkit.const.reg.transtype:TransType', 'UDP', 8, 'next header', {}), + ('make.type', 'header', None, 'type', + 'pcapkit.const.mh.packet:Packet', 'Fast_Neighbor_Advertisement', 8, 'MH type', {}), + # RFC 6275 section 6.1.8 and 6.1.9. + ('_make_msg_ba.status', 'message', 'Binding_Acknowledgement', 'status', + 'pcapkit.const.mh.status_code:StatusCode', 'Accepted_but_prefix_discovery_necessary', + 8, 'status', {}), + ('_make_msg_be.status', 'message', 'Binding_Error', 'status', + 'pcapkit.const.mh.binding_error:BindingError', 'Unrecognized_MH_Type_value', + 8, 'status', {}), + # RFC 5568 section 6.2.3, 6.2.1.1 and 6.2.1.2. + ('_make_msg_fback.status', 'message', 'Fast_Binding_Acknowledgment', 'status', + 'pcapkit.protocols.internet.mh:FastBindingAcknowledgmentStatus', 'Reason_unspecified', + 8, 'status', {}), + ('_make_msg_hi.code', 'message', 'Handover_Initiate_Message', 'code', + 'pcapkit.const.mh.handover_initiate_status:HandoverInitiateStatus', + 'FBU_whose_source_IP_address_is_not_PCoA', 8, 'code', {}), + ('_make_msg_hack.code', 'message', 'Handover_Acknowledge_Message', 'code', + 'pcapkit.const.mh.handover_ack_status:HandoverACKStatus', + 'Handover_Accepted_NCoA_not_valid', 8, 'code', {}), + # RFC 5846 section 5.1 and 5.2: the octet after the B.R. Type draws from the + # registry that type names. + ('_make_msg_brm.br_type', 'message', 'Binding_Revocation_Message', 'br_type', + 'pcapkit.const.mh.binding_revocation:BindingRevocation', + 'Binding_Revocation_Acknowledgement', 8, 'binding revocation type', {}), + ('_make_msg_brm.code (indication)', 'message', 'Binding_Revocation_Message', 'code', + 'pcapkit.const.mh.revocation_trigger:RevocationTrigger', 'Administrative_Reason', + 8, 'revocation trigger', {}), + ('_make_msg_brm.code (acknowledgement)', 'message', 'Binding_Revocation_Message', 'code', + 'pcapkit.const.mh.revocation_status_code:RevocationStatusCode', 'partial_success', + 8, 'status', {'br_type': 2}), + # RFC 6705 section 10.2. + ('_make_msg_lra.status', 'message', 'Localized_Routing_Acknowledgment', 'status', + 'pcapkit.protocols.internet.mh:LocalizedRoutingStatus', 'MN_Not_Attached', + 8, 'status', {}), + # RFC 7077 section 4.1 and 4.2; the notification reason is 16 bits. + ('_make_msg_upn.reason', 'message', 'Update_Notification', 'reason', + 'pcapkit.const.mh.upn_reason:UpdateNotificationReason', 'UPDATE_SESSION_PARAMETERS', + 16, 'notification reason', {}), + ('_make_msg_upa.status', 'message', 'Update_Notification_Acknowledgement', 'status', + 'pcapkit.const.mh.upa_status:UpdateNotificationACKStatus', 'Reason_unspecified', + 8, 'status', {}), + # RFC 7109 section 6.1.1 and 6.1.2; the flow binding type is 16 bits. + ('_make_msg_fbm.fb_type', 'message', 'Flow_Binding_Message', 'fb_type', + 'pcapkit.const.mh.fb_type:FlowBindingType', 'Acknowledgement', + 16, 'flow binding type', {}), + ('_make_msg_fbm.code (indication)', 'message', 'Flow_Binding_Message', 'code', + 'pcapkit.const.mh.fb_indication_trigger:FlowBindingIndicationTrigger', + 'Administrative_Reason', 8, 'trigger', {}), + ('_make_msg_fbm.code (acknowledgement)', 'message', 'Flow_Binding_Message', 'code', + 'pcapkit.const.mh.fb_ack_status:FlowBindingACKStatus', 'Binding', + 8, 'status', {'fb_type': 2}), + # Options, each after the 8-octet Binding Refresh Request. + ('_make_opt_mn_id.subtype', 'option', 'MN_ID_OPTION_TYPE', 'subtype', + 'pcapkit.const.mh.mn_id_subtype:MNIDSubtype', 'IPv6_Address', 8, 'MN-ID subtype', {}), + ('_make_opt_auth.subtype', 'option', 'AUTH_OPTION_TYPE', 'subtype', + 'pcapkit.const.mh.auth_subtype:AuthSubtype', 'MN_AAA', 8, 'subtype', + {'data': b'\xa5\xa5'}), + ('_make_opt_ipv6_ap.code', 'option', 'Mobility_Header_IPv6_Address_Prefix', 'code', + 'pcapkit.protocols.internet.mh:IPv6AddressPrefixCode', 'NAR_Prefix', 8, 'code', {}), + ('_make_opt_dns.status', 'option', 'DNS_UPDATE_TYPE', 'status', + 'pcapkit.const.mh.dns_status_code:DNSStatusCode', 'Reason_unspecified', 8, 'status', {}), + ('_make_opt_hi.hi', 'option', 'Handoff_Indicator_Option', 'hi', + 'pcapkit.const.mh.handoff_type:HandoffType', + 'Handoff_between_two_different_interfaces_of_the_mobile_node', 8, 'handoff type', {}), + ('_make_opt_att.att', 'option', 'Access_Technology_Type_Option', 'att', + 'pcapkit.const.mh.access_type:AccessType', 'IEEE_802_3', 8, 'access technology type', {}), + ('_make_opt_ipv4_ack.status', 'option', 'IPv4_Address_Acknowledgement', 'status', + 'pcapkit.const.mh.dsmipv6_home_address:DSMIPv6HomeAddress', 'Failure_reason_unspecified', + 8, 'status', {}), + ('_make_opt_bid.status', 'option', 'Binding_Identifier', 'status', + 'pcapkit.const.mh.status_code:StatusCode', 'Accepted_but_prefix_discovery_necessary', + 8, 'status', {}), + ('_make_opt_ipv4_hoa_rep.status', 'option', 'IPv4_Home_Address_Reply', 'status', + 'pcapkit.const.mh.home_address_reply:HomeAddressReply', 'Failure_Reason_Unspecified', + 8, 'status', {}), + # RFC 5844 section 3.4: the mode is the one-bit S flag. + ('_make_opt_ipv4_dhcp.mode', 'option', 'IPv4_DHCP_Support_Mode', 'mode', + 'pcapkit.const.mh.dhcp_support_mode:DHCPSupportMode', 'S_flag', 1, 'DHCP support mode', {}), + ('_make_opt_lmaa.code', 'option', 'Local_Mobility_Anchor_Address_Option', 'code', + 'pcapkit.protocols.internet.mh:LMAAddressCode', 'IPv4_LMAA', 8, 'code', + {'address': '10.0.0.1'}), + ('_make_opt_fid.status', 'option', 'Flow_Identification_Mobility_Option', 'status', + 'pcapkit.const.mh.flow_id_status:FlowIDStatus', 'Administratively_prohibited', + 8, 'status', {}), + ('_make_opt_mn_group.subtype', 'option', 'Mobile_Node_Group_Identifier', 'subtype', + 'pcapkit.const.mh.mn_group_id:MNGroupID', 'Bulk_Binding_Update_Group', + 8, 'group identifier subtype', {}), + ('_make_opt_mag_mp.att', 'option', 'MAG_Multipath_Binding', 'att', + 'pcapkit.const.mh.access_type:AccessType', 'IEEE_802_3', 8, 'access technology type', {}), + ('_make_opt_mag_id.subtype', 'option', 'MAG_Identifier', 'subtype', + 'pcapkit.const.mh.mn_id_subtype:MNIDSubtype', 'IMSI', 8, 'MAG identifier subtype', {}), + # RFC 5949 section 6.2.4: each Req-type is a mobility option type. + ('_make_opt_cr.requests', 'request', 'Context_Request_Option', 'requests', + 'pcapkit.const.mh.option:Option', 'MN_ID_OPTION_TYPE', 8, 'request type', {}), + # Sub-options, through the option that carries them. + ('_make_fid_suboption.ts_format', 'suboption', + ('Flow_Identification_Mobility_Option', 'suboptions', + 'pcapkit.const.mh.flow_id_suboption:FlowIDSuboption', 'Traffic_Selector'), 'ts_format', + 'pcapkit.const.mh.traffic_selector:TrafficSelector', 'IPv4_Binary_Traffic_Selector', + 8, 'TS format', {}), + ('_make_fid_suboption.action', 'suboption', + ('Flow_Identification_Mobility_Option', 'suboptions', + 'pcapkit.const.mh.flow_id_suboption:FlowIDSuboption', 'Flow_Binding_Action'), 'action', + 'pcapkit.const.mh.fb_action:FlowBindingAction', 'Delete', 8, 'action', {}), + ('_make_ani_suboption.op_id_type', 'suboption', + ('Access_Network_Identifier', 'suboptions', + 'pcapkit.const.mh.ani_suboption:ANISuboption', 'Operator_Identifier'), 'op_id_type', + 'pcapkit.const.mh.operator_id:OperatorID', + 'Operator_Identifier_as_a_variable_length_Private_Enterprise_Number', 8, 'Op-ID type', {}), + ('_make_qos_attribute.ts_format', 'suboption', + ('Quality_of_Service', 'attributes', + 'pcapkit.const.mh.qos_attribute:QoSAttribute', 'QoS_Traffic_Selector'), 'ts_format', + 'pcapkit.const.mh.traffic_selector:TrafficSelector', 'IPv4_Binary_Traffic_Selector', + 8, 'TS format', {}), +) + +#: ``(maker, carrier, enumeration, member, bits, name)``, one row per list of +#: ``(type, kwargs)`` pairs. ``carrier`` is :data:`None` for the options of the +#: message, or the option and its list argument; ``CGA_Parameters`` carries +#: the extensions of its one CGA parameter. ``bits`` is the type's width in the +#: schema and ``name`` the field's name in the error message. +LIST_ROWS = ( + # RFC 6275 section 6.2.1: Option Type is 8 bits. + ('_make_mh_options', None, 'pcapkit.const.mh.option:Option', 'Pad1', 8, 'option type'), + # RFC 6089 section 4.2.1, RFC 6757 section 3.1, RFC 7222 section 4.2 and + # RFC 8127 section 3.1: each sub-option or attribute type is 8 bits. + ('_make_fid_suboptions', ('Flow_Identification_Mobility_Option', 'suboptions'), + 'pcapkit.const.mh.flow_id_suboption:FlowIDSuboption', 'BID_Reference', 8, + 'sub-option type'), + ('_make_ani_suboptions', ('Access_Network_Identifier', 'suboptions'), + 'pcapkit.const.mh.ani_suboption:ANISuboption', 'Geo_Location', 8, 'sub-option type'), + ('_make_qos_attributes', ('Quality_of_Service', 'attributes'), + 'pcapkit.const.mh.qos_attribute:QoSAttribute', 'Guaranteed_DL_Bit_Rate', 8, + 'attribute type'), + ('_make_lcmp_suboptions', ('LMA_Controlled_MAG_Parameters', 'suboptions'), + 'pcapkit.const.mh.lma_mag_suboption:LMAControlledMAGSuboption', 'Heartbeat_Control', 8, + 'sub-option type'), + # RFC 4581 section 2: the CGA Extension Type is 16 bits. + ('_make_cga_extensions', ('CGA_Parameters', 'extensions'), + 'pcapkit.const.mh.cga_extension:CGAExtension', 'Multi_Prefix', 16, 'extension type'), +) + +#: Rows the widest code cannot build and parse back through, and why. +WIDEST_EXEMPT = { + # Closed enumerations: a parse refuses a code no member holds (#877, #2477). + '_make_msg_fback.status', '_make_msg_lra.status', '_make_opt_ipv6_ap.code', + # RFC 5949 section 6.2.2 ties each code to one address family (#2280). + '_make_opt_lmaa.code', +} + +#: Other arguments a row's widest code needs: an unassigned MN-ID subtype +#: carries its identifier as octets. +WIDEST_BASE = { + '_make_opt_mn_id.subtype': {'identifier': b'\x01'}, +} + + +def widest(row: 'tuple') -> 'tuple': + """``row`` with the other arguments its widest code needs.""" + return (*row[:-1], {**row[-1], **WIDEST_BASE.get(row[0], {})}) + + +def load(spec: 'str') -> 'type': + """The class ``spec`` names as ``module:class``.""" + module, _, name = spec.partition(':') + return getattr(importlib.import_module(module), name) + + +class MHMakeCodeTests(unittest.TestCase): + """Each enumeration field of MH's makers, one row at a time.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def _build(self, row: 'tuple', value: 'object') -> 'bytes': + """The octets of an MH header whose ``row`` field is ``value``.""" + from pcapkit.const.mh.option import Option + from pcapkit.const.mh.packet import Packet + from pcapkit.protocols.internet.mh import MH + + _, kind, target, field, *_, base = row + header = {'next': 59, 'type': Packet.Binding_Refresh_Request, + 'chksum': b'\x00\x00', 'payload': b''} # type: dict[str, object] + if kind == 'header': + header[field] = value + elif kind == 'message': + header.update(type=Packet[target], data={**base, field: value}) + elif kind == 'option': + header['data'] = {'options': [(Option[target], {**base, field: value})]} + elif kind == 'request': + header['data'] = {'options': [(Option[target], {field: [(value, b'')]})]} + else: + option, key, sub_spec, sub = target + header['data'] = {'options': [(Option[option], { + key: [(load(sub_spec)[sub], {**base, field: value})]})]} + return bytes(MH(**header)) + + def _read(self, row: 'tuple', octets: 'bytes') -> 'object': + """The ``row`` field as ``octets`` parse it back.""" + from pcapkit.const.mh.option import Option + from pcapkit.protocols.internet.mh import MH + + _, kind, target, field, *_ = row + info = MH(io.BytesIO(octets), len(octets), extension=True).info + if kind in ('header', 'message'): + return getattr(info, field) + if kind != 'suboption': + option = info.options[Option[target]] + return option.requests[0].type if kind == 'request' else getattr(option, field) + name, key, sub_spec, sub = target + return getattr(getattr(info.options[Option[name]], key)[load(sub_spec)[sub]], field) + + def assertRefused(self, row: 'tuple', value: 'object') -> None: + """The ``row`` field refuses ``value`` with a ProtocolError naming it.""" + from pcapkit.utilities.exceptions import ProtocolError + + with self.assertRaises(ProtocolError) as caught: + self._build(row, value) + self.assertEqual(str(caught.exception), f'MH: invalid {row[7]}: {value!r}') + + def test_a_code_too_wide_for_its_field_is_refused(self) -> None: + for row in ROWS: + with self.subTest(row=row[0]): + self.assertRefused(row, 1 << row[6]) + + def test_a_bool_is_refused_for_a_field_wider_than_one_bit(self) -> None: + for row in ROWS: + with self.subTest(row=row[0]): + if row[6] > 1: + self.assertRefused(row, True) + else: + # A one-bit field takes a flag, as Protocol._make_uint lets it. + self.assertEqual(self._build(row, True), self._build(row, 1)) + + def test_a_name_is_refused(self) -> None: + for row in ROWS: + with self.subTest(row=row[0]): + self.assertRefused(row, row[5]) + + def test_a_member_and_its_code_build_the_same_octets(self) -> None: + for row in ROWS: + member = load(row[4])[row[5]] + with self.subTest(row=row[0]): + octets = self._build(row, member) + self.assertEqual(self._build(row, int(member)), octets) + self.assertEqual(self._read(row, octets), member) + + def test_a_name_resolved_first_builds_the_members_octets(self) -> None: + # The path the docstrings give a caller holding the name. + from pcapkit.protocols.internet.mh import MH + + for row in ROWS: + namespace = load(row[4]) + with self.subTest(row=row[0]): + self.assertEqual(self._build(row, MH.resolve_index(row[5], namespace=namespace)), + self._build(row, namespace[row[5]])) + + def test_the_widest_code_builds_parses_back_and_rebuilds(self) -> None: + # #2594: the field width decides what is refused, so the widest code + # builds, even where no member holds it. + from pcapkit.protocols.internet.mh import MH + + for row in (widest(row) for row in ROWS if row[0] not in WIDEST_EXEMPT): + code = (1 << row[6]) - 1 + with self.subTest(row=row[0], code=code): + octets = self._build(row, code) + self.assertEqual(int(self._read(row, octets)), code) # type: ignore[call-overload] + info = MH(io.BytesIO(octets), len(octets), extension=True).info + rebuilt = bytes(MH(next=info.next, type=info.type, chksum=info.chksum, + data=info, payload=b'')) + self.assertEqual(rebuilt.hex(), octets.hex()) + + def test_a_parsed_code_builds_the_same_octets(self) -> None: + # #2594: make() takes the value a parse hands back, also where no + # member holds it, such as an UPN reason or FB type past 255. + for row in (widest(row) for row in ROWS if row[0] not in WIDEST_EXEMPT): + code = (1 << row[6]) - 1 + with self.subTest(row=row[0], code=code): + octets = self._build(row, code) + self.assertEqual(self._build(row, self._read(row, octets)).hex(), octets.hex()) + + def test_the_binding_error_status_is_a_binding_error(self) -> None: + # RFC 6275 section 6.1.9: the BE status draws from BindingError, the + # registry its schema and data model hold, not from StatusCode. + from pcapkit.const.mh.binding_error import BindingError + from pcapkit.const.mh.packet import Packet + from pcapkit.protocols.internet.mh import MH + + for value in (2, BindingError.Unrecognized_MH_Type_value): + with self.subTest(value=value): + schema = object.__new__(MH).make(next=59, type=Packet.Binding_Error, + chksum=b'\x00\x00', data={'status': value}) + status = schema['data']['status'] + self.assertIs(type(status), BindingError) + self.assertEqual(status, BindingError.Unrecognized_MH_Type_value) + + def test_no_maker_takes_a_removed_keyword(self) -> None: + """#2594: no maker's signature declares a ``*_default``, ``*_namespace`` or ``*_reversed`` keyword.""" + from pcapkit.protocols.internet.mh import MH + + for name in sorted(vars(MH)): + if name != 'make' and not name.startswith('_make_'): + continue + func = getattr(MH, name) + if not callable(func): + continue + for param in inspect.signature(func).parameters: + with self.subTest(maker=name, argument=param): + self.assertFalse(param.endswith(('_default', '_namespace', '_reversed'))) + + +class MHListTypeTests(unittest.TestCase): + """The type of each item of an option, sub-option, attribute or extension list.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def _octets(self, row: 'tuple', items: 'object') -> 'bytes': + """The octets of an MH header carrying ``items`` in the ``row`` list.""" + from pcapkit.const.mh.cga_type import CGAType + from pcapkit.const.mh.option import Option + from pcapkit.const.mh.packet import Packet + from pcapkit.protocols.data.internet.mh import CGAParameter + from pcapkit.protocols.internet.mh import MH + + carrier = row[1] + if carrier is None: + options = items + elif carrier[0] == 'CGA_Parameters': + modifier = CGAType.Tag_086F_CA5E_10B2_00C9_9C8C_E001_6427_7C08 # type: Any + param = CGAParameter(modifier=modifier, prefix=1, collision_count=1, + public_key=b'\x30\x00', extensions=items) # type: ignore[arg-type] + options = [(Option.CGA_Parameters, {'parameters': [param]})] + else: + options = [(getattr(Option, carrier[0]), {carrier[1]: items})] + return bytes(MH(next=59, type=Packet.Binding_Refresh_Request, chksum=b'\x00\x00', + payload=b'', data={'options': options})) + + def _parsed(self, row: 'tuple', octets: 'bytes') -> 'Any': + """The ``row`` list as ``octets`` parse it back.""" + from pcapkit.const.mh.option import Option + from pcapkit.protocols.internet.mh import MH + + info = MH(io.BytesIO(octets), len(octets), extension=True).info # type: Any + options = info.options + carrier = row[1] + if carrier is None: + return options + if carrier[0] == 'CGA_Parameters': + return options[Option.CGA_Parameters].parameters[0].extensions + return getattr(options[getattr(Option, carrier[0])], carrier[1]) + + def _builders(self, row: 'tuple') -> 'dict[str, Any]': + """A builder for each form the ``row`` list takes a type in.""" + from pcapkit.corekit.multidict import OrderedMultiDict + + octets = self._octets(row, [((1 << row[4]) - 1, {})]) + _, item = list(self._parsed(row, octets).items(multi=True))[0] + return { + 'pair': lambda value: self._octets(row, [(value, {})]), + 'parsed': lambda value: self._octets(row, OrderedMultiDict([(value, item)])), + } + + def assertRefused(self, row: 'tuple', build: 'Any', value: 'object') -> None: + """``build(value)`` raises a ProtocolError naming the ``row`` field, and nothing else.""" + from pcapkit.utilities.exceptions import ProtocolError + + message = f'MH: invalid {row[5]}: {value!r}' + try: + octets = build(value) + except ProtocolError as error: + self.assertEqual(str(error), message) + except Exception as error: # pylint: disable=broad-except + self.fail(f'{value!r} raised {type(error).__name__}: {error}; expected {message}') + else: + self.fail(f'{value!r} was accepted, building {octets.hex()}; expected {message}') + + def test_a_type_the_field_cannot_carry_is_refused(self) -> None: + # #2594: a type checked as the field width checks it, before anything + # reads it as a registry key or a padding code. + for row in LIST_ROWS: + for form, build in self._builders(row).items(): + for value in (1 << row[4], -1, True, row[3]): + with self.subTest(maker=row[0], form=form, value=value): + self.assertRefused(row, build, value) + + def test_the_widest_type_builds_parses_back_and_rebuilds(self) -> None: + from pcapkit.protocols.internet.mh import MH + + for row in LIST_ROWS: + code = (1 << row[4]) - 1 + for form, build in self._builders(row).items(): + with self.subTest(maker=row[0], form=form): + octets = build(code) + # the options of the message end in the padding that aligns them + self.assertEqual(int(list(self._parsed(row, octets))[0]), code) + info = MH(io.BytesIO(octets), len(octets), extension=True).info + rebuilt = bytes(MH(next=info.next, type=info.type, chksum=info.chksum, + data=info, payload=b'')) + self.assertEqual(rebuilt.hex(), octets.hex()) + + def test_a_member_and_its_code_build_the_same_octets(self) -> None: + for row in LIST_ROWS: + namespace = load(row[2]) # type: Any + code = namespace.get((1 << row[4]) - 1) + for form, build in self._builders(row).items(): + with self.subTest(maker=row[0], form=form): + self.assertEqual(build(code).hex(), build(int(code)).hex()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/protocols/internet/test_mh_unit.py b/tests/protocols/internet/test_mh_unit.py index 9c9a2b5e47..478a00a7a6 100644 --- a/tests/protocols/internet/test_mh_unit.py +++ b/tests/protocols/internet/test_mh_unit.py @@ -1009,9 +1009,10 @@ def header(type_: Packet) -> SimpleNamespace: self.assertEqual(proto._make_msg_fback( None, status=FastBindingAcknowledgmentStatus.Insufficient_resources, options=[], ).status, 130) + # A name is resolved by the caller first, the maker refusing one (#2594). self.assertEqual(proto._make_msg_fback( - None, status='Administratively_prohibited', options=[], - status_namespace=FastBindingAcknowledgmentStatus, + None, options=[], status=MH.resolve_index('Administratively_prohibited', + namespace=FastBindingAcknowledgmentStatus), ).status, 129) self.assertEqual(proto._make_msg_fback(SimpleNamespace( status=FastBindingAcknowledgmentStatus.Fast_Binding_Update_accepted_but_NCoA_is_invalid, @@ -1087,7 +1088,7 @@ def header(type_: Packet) -> SimpleNamespace: ), header=header(Packet.Fast_Binding_Acknowledgment)) self.assertEqual(fback.lifetime, datetime.timedelta(seconds=100)) - # control: BU/BA still scale by 4 on read. + # control: BU/BA scale by 4 on read. bu = proto._read_msg_bu(SimpleNamespace( seq=1, flags={'A': 1, 'H': 1, 'L': 0, 'K': 0, 'M': 0, 'R': 0, 'P': 0, 'F': 0, 'T': 0, 'B': 0, 'S': 0, 'D': 0, 'reserved': 0}, @@ -1113,7 +1114,7 @@ def header(type_: Packet) -> SimpleNamespace: options=[]) self.assertEqual(made_fback.lifetime, 100) - # control: BU/BA still divide by 4 on make. + # control: BU/BA divide by 4 on make. made_bu = proto._make_msg_bu(None, seq=1, lifetime=datetime.timedelta(seconds=400), options=[]) self.assertEqual(made_bu.lifetime, 100) @@ -1212,8 +1213,8 @@ def test_mh_fmipv6_option_readers_constructors_and_guards(self) -> None: code=IPv6AddressPrefixCode.NAR_IP_address, ).code, 3) self.assertEqual(proto._make_opt_ipv6_ap( - Option.Mobility_Header_IPv6_Address_Prefix, code='Old_Care_of_Address', - code_namespace=IPv6AddressPrefixCode, + Option.Mobility_Header_IPv6_Address_Prefix, + code=MH.resolve_index('Old_Care_of_Address', namespace=IPv6AddressPrefixCode), ).code, 1) self.assertEqual(proto._make_opt_ipv6_ap( Option.Mobility_Header_IPv6_Address_Prefix, @@ -1561,7 +1562,7 @@ def handover_initiate(code: int) -> object: with self.assertRaises(EnumValueError): handover_initiate(wire) - with self.subTest('out-of-octet values are still rejected'): + with self.subTest('out-of-octet values are rejected'): for enum_cls in (FastBindingAcknowledgmentStatus, IPv6AddressPrefixCode): for value in (-1, 256): with self.assertRaises(EnumValueError): @@ -1664,7 +1665,7 @@ def test_mh_get_default_works_uniformly_through_the_inherited_method(self) -> No try: cls.get('bogus', member) except TypeError as error: - self.fail(f'{cls.__name__}.get() still rejects a default argument: {error}') + self.fail(f'{cls.__name__}.get() rejects a default argument: {error}') with self.subTest('a name miss falls back to a default naming a real member'): for cls in classes: @@ -1679,7 +1680,7 @@ def test_mh_get_default_works_uniformly_through_the_inherited_method(self) -> No member = next(iter(cls)) self.assertIs(cls.get(9999, member), member) - with self.subTest('a default naming no registered member still raises, not TypeError'): + with self.subTest('a default naming no registered member raises, not TypeError'): # The issue's own repro: a default that does not itself resolve # does not silently swallow the miss -- it falls through to the # same EnumKeyError ``key`` alone raises, uniformly on every class @@ -1690,7 +1691,7 @@ def test_mh_get_default_works_uniformly_through_the_inherited_method(self) -> No cls.get('bogus', 'also-bogus') self.assertIn('bogus', str(caught.exception)) - with self.subTest('an unusable default on an int value miss still raises EnumValueError'): + with self.subTest('an unusable default on an int value miss raises EnumValueError'): for cls in classes: with self.subTest(cls=cls.__name__): with self.assertRaises(EnumValueError): @@ -1748,7 +1749,7 @@ def test_mh_local_enums_raise_and_do_not_alias(self) -> None: with self.assertRaises(EnumKeyError): enum_cls.get('NOT_A_REAL_MEMBER') - with self.subTest('the shared -1 aliasing defect is gone'): + with self.subTest('no name miss mints a member at the shared -1'): # This guards against ``get('bogus_one')`` minting a member at the # shared sentinel -1, and ``get('bogus_two')`` then resolving to # that same member -- a name that lies about itself. Both calls @@ -1952,7 +1953,7 @@ def test_mh_padding_option_schema_sizes_itself(self) -> None: self.assertEqual(bytes(pad1), b'\x00') self.assertEqual(stream.tell(), 1) - # a PadN option still spans its length octet plus that many data octets + # a PadN option spans its length octet plus that many data octets stream = io.BytesIO(b'\x01\x02\xaa\xbb\xcc') padn = schema.PadOption.unpack(stream, 5, {}) self.assertEqual(padn.type, Option.PadN) @@ -2065,7 +2066,7 @@ def test_mh_opaque_message_body_is_padded_with_a_warning(self) -> None: self.assertIs(proto._pad_mh_message(body), body) warned.assert_not_called() - # and the whole header still comes out 8-octet aligned when it can be padded + # and the whole header comes out 8-octet aligned when it can be padded raw = bytes(MH(next=TransType.IPv6_NoNxt, type=Packet.Binding_Refresh_Request, chksum=b'\x00\x00', data={'options': []})) self.assertEqual(len(raw) % 8, 0) @@ -2360,7 +2361,7 @@ def test_mh_option_and_extension_length_helpers_do_not_share_a_header_width(self self.assertEqual( MH._mh_extension_length(stored) - MH._mh_option_length(stored), 2) - # a real mobility option still round-trips through the 2-octet helper + # a real mobility option round-trips through the 2-octet helper from pcapkit.protocols.schema.internet import mh as schema option = schema.BindingRefreshAdviceOption.unpack(bytes.fromhex('02020064')) self.assertEqual(MH._mh_option_length(option.length), 4) @@ -2882,7 +2883,7 @@ def test_mh_length_derived_addresses_reject_a_bool(self) -> None: self.assertIn(f'int({value!r})', str(context.exception)) # the escape hatch the message points at: int(True) is 1, which is a - # legitimate -- if unusual -- IPv4 address, and still sizes as one + # legitimate -- if unusual -- IPv4 address, and sizes as one schema = proto._make_opt_lmaa( # type: ignore[arg-type] Option.Local_Mobility_Anchor_Address_Option, address=int(True)) self.assertEqual(schema.length, 6) @@ -2897,15 +2898,15 @@ def test_mh_length_derived_addresses_reject_a_bool(self) -> None: self.assertIn('does not appear to be an IPv4 or IPv6 address', str(context.exception)) - def test_mh_bool_is_still_a_valid_value_for_a_code_field(self) -> None: - """#508 must not spread to a code field, where a :obj:`bool` means 0/1. + def test_mh_bool_is_refused_for_a_code_field_wider_than_one_bit(self) -> None: + """A :obj:`bool` is refused for a code field as for a number field. - ``BindingRevocationMessage.code`` is a + A number field wider than one bit, such as ``NonceIndicesOption.home``, + refuses a :obj:`bool` through ``Protocol._make_uint`` (#2614). A code + field does too, through ``Protocol._make_index``, rather than packing + ``True`` as ``1``: ``BindingRevocationMessage.code`` is a :class:`~pcapkit.corekit.fields.misc.SwitchField` whose branches are - EnumFields, so ``code=True`` packs as ``1``, and the guard for #508 is not - a blanket rejection there. A number field wider than one bit, such as - ``NonceIndicesOption.home``, refuses a :obj:`bool` through - ``Protocol._make_uint`` instead (#2614). + one-octet EnumFields (#2594, #2617). """ from pcapkit.const.mh.binding_revocation import BindingRevocation from pcapkit.const.mh.option import Option @@ -2922,17 +2923,15 @@ def test_mh_bool_is_still_a_valid_value_for_a_code_field(self) -> None: bytes.fromhex('040400010000')) # a code field, through a SwitchField whose branches are EnumFields + for br_type in (BindingRevocation.Binding_Revocation_Indication, + BindingRevocation.Binding_Revocation_Acknowledgement): + for flag in (True, False): + with self.subTest(br_type=br_type, code=flag), self.assertRaises(ProtocolError): + proto._make_msg_brm(br_type=br_type, code=flag, seq=1) # type: ignore[arg-type] self.assertEqual( proto._make_msg_brm( # type: ignore[arg-type] br_type=BindingRevocation.Binding_Revocation_Indication, - code=True, seq=1).pack(), - proto._make_msg_brm( # type: ignore[arg-type] - br_type=BindingRevocation.Binding_Revocation_Indication, - code=1, seq=1).pack()) - self.assertEqual( - proto._make_msg_brm( # type: ignore[arg-type] - br_type=BindingRevocation.Binding_Revocation_Indication, - code=True, seq=1).pack(), + code=1, seq=1).pack(), bytes.fromhex('010100010000')) def test_mh_dmnp_prefix_length_is_checked_against_its_field(self) -> None: @@ -2988,7 +2987,7 @@ def test_mh_bid_rejects_an_out_of_range_binding_priority(self) -> None: self.assertIsInstance(context.exception, BaseError) self.assertIn('invalid binding priority: 128', str(context.exception)) - # 0x7F is the largest value that fits, and it still takes an address + # 0x7F is the largest value that fits, and it takes an address schema = proto._make_opt_bid(Option.Binding_Identifier, # type: ignore[arg-type] bid=1, bid_pri=0x7F, address='198.51.100.7') self.assertEqual(schema.flags['BID_PRI'], 0x7F) @@ -3056,11 +3055,11 @@ def test_mh_lmaa_code_follows_the_address_family(self) -> None: schema = proto._make_opt_lmaa(lmaa, address=address, code=code) # type: ignore[arg-type] self.assertEqual(schema.pack().hex(), octets) with self.subTest(address=address, code='explicit, matching, by name'): - # #2280: a str code name has to resolve against LMAAddressCode even - # when code_namespace is not given, as every sibling _make_opt_* - # does -- _make_index's str resolution needs an enum default. + # #2280: a code name resolves against LMAAddressCode, through + # resolve_index since the maker refuses a name (#2594). name = 'IPv4_LMAA' if '.' in address else 'IPv6_LMAA' - schema = proto._make_opt_lmaa(lmaa, address=address, code=name) # type: ignore[arg-type] + code = MH.resolve_index(name, namespace=LMAAddressCode) + schema = proto._make_opt_lmaa(lmaa, address=address, code=code) # type: ignore[arg-type] self.assertEqual(schema.pack().hex(), octets) for address, code in (('10.0.0.1', LMAAddressCode.IPv6_LMAA), @@ -3075,13 +3074,14 @@ def test_mh_lmaa_code_follows_the_address_family(self) -> None: for address, name in (('10.0.0.1', 'IPv6_LMAA'), ('2001:db8::1', 'IPv4_LMAA')): with self.subTest(address=address, code=name): + code = MH.resolve_index(name, namespace=LMAAddressCode) with self.assertRaises(ProtocolError) as context: - proto._make_opt_lmaa(lmaa, address=address, code=name) # type: ignore[arg-type] + proto._make_opt_lmaa(lmaa, address=address, code=code) # type: ignore[arg-type] self.assertIsInstance(context.exception, BaseError) self.assertIn('invalid code: code does not match address family:', str(context.exception)) - # a captured mismatch is data, not a request, so it still rebuilds as read + # a captured mismatch is data, not a request, so it rebuilds as read captured = bytes.fromhex('290601000a000001') data = proto._read_opt_lmaa( # type: ignore[arg-type] LMAAddressOption.unpack(captured), options=None) @@ -3244,7 +3244,7 @@ def test_mh_mn_id_option_converts_int_identifier_per_subtype(self) -> None: # ``NAI`` is the one subtype with no non-arbitrary int-to-wire mapping # -- its field is text (RFC 4283's ``user@realm``), not a number -- so - # an ``int`` is still rejected there, with a message naming the + # an ``int`` is rejected there, with a message naming the # decimal-string alternative, which is itself checked to actually work. with self.assertRaises(ProtocolError) as ctx: proto._make_opt_mn_id( # type: ignore[arg-type] @@ -3312,12 +3312,10 @@ def test_mh_mn_id_option_converts_int_identifier_per_subtype(self) -> None: self.assertEqual(schema.length, 1 + id_len) self.assertEqual(len(schema.pack()), schema.length + 2) - # 0, a negative value, and anything above 255 are all outside what - # ``MNIDSubtype._missing_`` extends. Naming the subtype in a rejection - # message must not let that enum round-trip's own bare ``ValueError`` - # escape in its place. 0 fits the one-octet Subtype and falls back to - # the generic ``BytesField``; the rest do not fit it, and are refused - # rather than written wrapped (#2614). + # 0 fits the one-octet Subtype but no ``MNIDSubtype`` member holds it, so + # it is written as given and falls back to the generic ``BytesField`` + # (#2594); a negative value and anything above 255 do not fit it, and + # are refused rather than written wrapped (#2614). schema = proto._make_opt_mn_id( # type: ignore[arg-type] Option.MN_ID_OPTION_TYPE, subtype=0, identifier=0x1234) self.assertEqual(schema.length, 3) @@ -3476,7 +3474,7 @@ def test_mh_mn_id_option_rejects_a_bool_identifier_for_every_subtype(self) -> No identifier=identifier) self.assertIn('must not be a bool', str(ctx.exception)) - # ``int(flag)`` is the documented escape hatch and still converts. + # ``int(flag)`` is the documented escape hatch and converts. schema = proto._make_opt_mn_id( # type: ignore[arg-type] Option.MN_ID_OPTION_TYPE, subtype=MNIDSubtype.IMSI, identifier=int(True)) diff --git a/tests/protocols/link/test_ieee802_11_make_codes_2594_unit.py b/tests/protocols/link/test_ieee802_11_make_codes_2594_unit.py new file mode 100644 index 0000000000..3ebca7b4d0 --- /dev/null +++ b/tests/protocols/link/test_ieee802_11_make_codes_2594_unit.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +"""IEEE 802.11 ``make()`` takes a type and a subtype as a member or a code (GitHub issue #2594). + +Owner's ruling (#2593, #2594), paraphrased: a ``make()`` field holding an enumeration +takes the member or an :class:`int`, resolved with the enumeration's ``get``, and no +maker takes a ``*_default``, ``*_namespace`` or ``*_reversed`` keyword. #2617: a code +the field cannot carry, or a :obj:`bool` for a field wider than +one bit, raises :exc:`~pcapkit.utilities.exceptions.ProtocolError`. + +:data:`ROWS` holds the Frame Control type, then the subtype once per type, since the +subtype resolves against the registry the type selects. Every case builds its own +frame in memory and reads no capture. + +""" + +import importlib +import unittest +from typing import TYPE_CHECKING + +from tests._support import reimport_once_per_class + +if TYPE_CHECKING: + from typing import Any + +#: ``(label, keyword, enumeration, bits, member, the other keywords)``. +ROWS = ( + ('type', 'type', 'pcapkit.const.ieee802_11.type.Type', 2, 'Control', + {'subtype': 13}), + ('subtype/Management', 'subtype', 'pcapkit.const.ieee802_11.management_subtype.ManagementSubtype', 4, + 'Beacon', {'type': 0}), + ('subtype/Control', 'subtype', 'pcapkit.const.ieee802_11.control_subtype.ControlSubtype', 4, + 'ACK', {'type': 1}), + ('subtype/Data', 'subtype', 'pcapkit.const.ieee802_11.data_subtype.DataSubtype', 4, + 'QoS_Data', {'type': 2}), + ('subtype/Extension', 'subtype', 'pcapkit.const.ieee802_11.extension_subtype.ExtensionSubtype', 4, + 'S1G_Beacon', {'type': 3}), +) # type: tuple[tuple[str, str, str, int, str, dict[str, Any]], ...] + + +def _attr(path: 'str') -> 'Any': + module, _, name = path.rpartition('.') + return getattr(importlib.import_module(module), name) + + +class TestIEEE802_11MakeCodes(unittest.TestCase): + """``type`` and ``subtype`` refuse what their field cannot carry.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def _build(self, **kwargs: 'Any') -> 'bytes': + return _attr('pcapkit.protocols.link.ieee802_11.IEEE802_11')(**kwargs).data + + def test_refusals(self) -> None: + exc = _attr('pcapkit.utilities.exceptions.ProtocolError') + for label, keyword, enum_path, bits, member, extra in ROWS: + for value in (1 << bits, True, member): + with self.subTest(row=label, value=value): + with self.assertRaisesRegex(exc, rf'^802\.11: invalid {keyword}: '): + self._build(**extra, **{keyword: value}) + + def test_member_and_code_build_the_same(self) -> None: + for label, keyword, enum_path, _, member, extra in ROWS: + with self.subTest(row=label): + value = getattr(_attr(enum_path), member) + built = self._build(**extra, **{keyword: value}) + self.assertEqual(self._build(**extra, **{keyword: int(value)}), built) + self.assertNotEqual(self._build(**extra), built) + + def test_removed_keywords_are_refused(self) -> None: + exc = _attr('pcapkit.utilities.exceptions.UnsupportedCall') + for field in ('type', 'subtype'): + for suffix, value in (('default', 0), ('namespace', {0: 'X'}), ('reversed', True)): + with self.subTest(keyword=f'{field}_{suffix}'): + with self.assertRaises(exc): + self._build(**{f'{field}_{suffix}': value}) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/protocols/link/test_ieee802_11_unit.py b/tests/protocols/link/test_ieee802_11_unit.py index ffc61d0b89..4b4f7a3c04 100644 --- a/tests/protocols/link/test_ieee802_11_unit.py +++ b/tests/protocols/link/test_ieee802_11_unit.py @@ -320,9 +320,11 @@ def test_every_type_subtype_and_flag_round_trips(self) -> None: def test_make_zero_fills_what_the_frame_carries(self) -> None: """Sequence Control, QoS Control and HT Control left out of ``make()`` are written as zeros where the header carries them.""" + from pcapkit.const.ieee802_11.data_subtype import DataSubtype + from pcapkit.const.ieee802_11.type import Type from pcapkit.protocols.link.ieee802_11 import IEEE802_11 - made = IEEE802_11(type='Data', subtype='QoS_Data', order=True) + made = IEEE802_11(type=Type.Data, subtype=DataSubtype.QoS_Data, order=True) self.assertEqual(made.data, bytes.fromhex('8880') + bytes(2 + 18 + 2 + 2 + 4)) info = made.info self.assertEqual((info.seq_ctrl.fragment_number, info.seq_ctrl.sequence_number), (0, 0)) @@ -405,6 +407,7 @@ def test_truncated_header_rebuilds_as_captured(self) -> None: def test_make_then_parse(self) -> None: """``make()`` lays out the fields Frame Control declares, zero-filling those not given, and parses back to the same octets.""" + from pcapkit.const.ieee802_11.control_subtype import ControlSubtype from pcapkit.const.ieee802_11.data_subtype import DataSubtype from pcapkit.const.ieee802_11.type import Type from pcapkit.protocols.link.ieee802_11 import IEEE802_11 @@ -420,7 +423,7 @@ def test_make_then_parse(self) -> None: self.assertEqual(made.data.hex(), expected.hex()) self._parse(made.data) - self.assertEqual(IEEE802_11(type='Control', subtype='ACK', addr1=_mac(1)).data, + self.assertEqual(IEEE802_11(type=Type.Control, subtype=ControlSubtype.ACK, addr1=_mac(1)).data, bytes.fromhex('d4000000') + ADDR[0]) self.assertEqual(IEEE802_11(type=0, subtype=8).data, bytes.fromhex('80000000') + bytes(20)) diff --git a/tests/protocols/link/test_link_make_codes_2594_unit.py b/tests/protocols/link/test_link_make_codes_2594_unit.py new file mode 100644 index 0000000000..23579e40b1 --- /dev/null +++ b/tests/protocols/link/test_link_make_codes_2594_unit.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +"""GitHub issue #2594: the link makers take an enumeration field as a member or a code. + +Each enumeration field of the ARP, Ethernet, GRE, L2TPv2, VLAN tag and Linux +cooked-capture makers goes through +:meth:`~pcapkit.protocols.protocol.Protocol._make_index`, which resolves a member +or an :class:`int` with the enumeration's ``get``. A name is refused rather than +resolved against an enumeration inferred from the caller's frame, and so are a +:obj:`bool` for a field wider than one bit and a code that does not fit the field +(#2617), where a maker wrote ``1`` for :data:`True` and a 16-bit field wrapped a +code too wide for it. Every refusal is a +:exc:`~pcapkit.utilities.exceptions.ProtocolError`. + +``ROWS`` holds one row per class and field, the VLAN maker under both of its tags; +``test_every_enumeration_argument_has_a_row`` finds the enumeration arguments from +the maker signatures, so an argument added to a maker needs a row. A caller holding a name resolves it first, with +:meth:`~pcapkit.protocols.protocol.Protocol.resolve_index`. + +Every case builds its own octets in memory and reads no capture. The classes are +imported inside each test, after :func:`~tests._support.reimport_once_per_class`. + +""" + +import importlib +import inspect +import unittest +from typing import TYPE_CHECKING, NamedTuple + +from tests._support import reimport_once_per_class + +if TYPE_CHECKING: + from typing import Any + + +class Row(NamedTuple): + """One enumeration field of one maker.""" + + #: ``(module, class)`` of the protocol whose ``make()`` is called. + cls: 'tuple[str, str]' + #: The argument of ``make()``; ``tci.pcp`` is the ``pcp`` of the ``tci`` argument. + field: 'str' + #: ``(module, class)`` of the enumeration the field holds. + enum: 'tuple[str, str]' + #: Width of the field on the wire, in bits. + bits: 'int' + #: The member built with; not the argument's default. + member: 'str' + #: Dotted path of the field in the parsed ``info``. + info: 'str' + #: Other arguments the member needs to build. + extra: 'dict[str, Any]' = {} + + +ARP = ('pcapkit.protocols.link.arp', 'ARP') +ETHERNET = ('pcapkit.protocols.link.ethernet', 'Ethernet') +GRE = ('pcapkit.protocols.link.gre', 'GRE') +L2TPV2 = ('pcapkit.protocols.link.l2tpv2', 'L2TPv2') +C_TAG = ('pcapkit.protocols.link.c_tag', 'C_Tag') +S_TAG = ('pcapkit.protocols.link.s_tag', 'S_Tag') +LINUX_SLL1 = ('pcapkit.protocols.link.linux_sll1', 'LinuxSLL1') +LINUX_SLL2 = ('pcapkit.protocols.link.linux_sll2', 'LinuxSLL2') + +HARDWARE = ('pcapkit.const.arp.hardware', 'Hardware') +OPERATION = ('pcapkit.const.arp.operation', 'Operation') +ETHERTYPE = ('pcapkit.const.reg.ethertype', 'EtherType') +L2TP_TYPE = ('pcapkit.const.l2tp.type', 'Type') +PRIORITY = ('pcapkit.const.vlan.priority_level', 'PriorityLevel') + +#: LLDP, an EtherType no layer is registered for, so the payload parses as nothing. +LLDP = 'IEEE_Std_802_1AB_Link_Layer_Discovery_Protocol' + +#: Widths from each schema, as RFC 826, IEEE 802.3, RFC 2784, RFC 2661 section +#: 3.1, IEEE 802.1Q and the tcpdump ``LINKTYPE_LINUX_SLL``/``SLL2`` pages give them. +ROWS = ( + Row(ARP, 'htype', HARDWARE, 16, 'Frame_Relay', 'htype'), + Row(ARP, 'ptype', ETHERTYPE, 16, 'Internet_Protocol_version_6', 'ptype', + {'spa': '2001:db8::1', 'tpa': '2001:db8::2'}), + Row(ARP, 'oper', OPERATION, 16, 'REPLY', 'oper'), + Row(ETHERNET, 'type', ETHERTYPE, 16, LLDP, 'type'), + Row(GRE, 'protocol_type', ETHERTYPE, 16, LLDP, 'protocol_type'), + Row(L2TPV2, 'type', L2TP_TYPE, 1, 'Control', 'flags.type'), + Row(C_TAG, 'pcp', PRIORITY, 3, 'VO', 'tci.pcp'), + Row(C_TAG, 'tci.pcp', PRIORITY, 3, 'VO', 'tci.pcp'), + Row(C_TAG, 'type', ETHERTYPE, 16, LLDP, 'type'), + Row(S_TAG, 'pcp', PRIORITY, 3, 'VO', 'tci.pcp'), + Row(S_TAG, 'tci.pcp', PRIORITY, 3, 'VO', 'tci.pcp'), + Row(S_TAG, 'type', ETHERTYPE, 16, LLDP, 'type'), + Row(LINUX_SLL1, 'arphrd_type', HARDWARE, 16, 'Frame_Relay', 'arphrd_type'), + Row(LINUX_SLL1, 'protocol_type', ETHERTYPE, 16, LLDP, 'protocol_type'), + Row(LINUX_SLL2, 'arphrd_type', HARDWARE, 16, 'Frame_Relay', 'arphrd_type'), + Row(LINUX_SLL2, 'protocol_type', ETHERTYPE, 16, LLDP, 'protocol_type'), +) + + +def load(path: 'tuple[str, str]') -> 'Any': + """The attribute ``path`` names, imported afresh.""" + return getattr(importlib.import_module(path[0]), path[1]) + + +def arguments(row: 'Row', value: 'Any') -> 'dict[str, Any]': + """The arguments of ``make()`` that give ``row``'s field ``value``.""" + if row.field == 'tci.pcp': + return {'tci': {'pcp': value, 'dei': False, 'vid': 0}, **row.extra} + return {row.field: value, **row.extra} + + +class TestLinkMakeCodes(unittest.TestCase): + """Pin how each link maker takes its enumeration fields.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def build(self, row: 'Row', value: 'Any') -> 'bytes': + """The octets ``row``'s maker builds with ``value`` for its field.""" + return load(row.cls)(**arguments(row, value)).data + + def test_a_code_wider_than_the_field_is_refused(self) -> None: + from pcapkit.utilities.exceptions import ProtocolError + + for row in ROWS: + with self.subTest(cls=row.cls[1], field=row.field, code=1 << row.bits): + with self.assertRaises(ProtocolError): + self.build(row, 1 << row.bits) + + def test_a_bool_is_refused_for_a_wider_field(self) -> None: + from pcapkit.utilities.exceptions import ProtocolError + + rows = [row for row in ROWS if row.bits > 1] + self.assertTrue(rows) + for row in rows: + with self.subTest(cls=row.cls[1], field=row.field): + with self.assertRaises(ProtocolError): + self.build(row, True) + + def test_a_name_is_refused(self) -> None: + from pcapkit.utilities.exceptions import ProtocolError + + for row in ROWS: + with self.subTest(cls=row.cls[1], field=row.field, name=row.member): + with self.assertRaises(ProtocolError): + self.build(row, row.member) + + def test_the_member_and_its_code_build_the_same_octets(self) -> None: + for row in ROWS: + member = getattr(load(row.enum), row.member) + with self.subTest(cls=row.cls[1], field=row.field, member=member): + made = self.build(row, member) + self.assertEqual(made, self.build(row, int(member))) + + # the octets carry the member's code, read back from the field + parsed = load(row.cls)(made, len(made)) + value = parsed.info + for part in row.info.split('.'): + value = getattr(value, part) + self.assertEqual(int(value), int(member)) + + def test_every_enumeration_argument_has_a_row(self) -> None: + """Found from the signatures, with no ``_default``, ``_namespace`` or ``_reversed`` left.""" + for path in sorted({row.cls for row in ROWS}): + params = inspect.signature(load(path).make).parameters + with self.subTest(cls=path[1]): + found = {name for name, param in params.items() + if str(param.annotation).startswith('Enum_')} + rows = {row.field for row in ROWS if row.cls == path and row.field != 'tci.pcp'} + self.assertEqual(found, rows) + self.assertEqual([name for name in params + if name.endswith(('_default', '_namespace', '_reversed'))], []) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/protocols/misc/checksum/test_ip_pseudo_2471_unit.py b/tests/protocols/misc/checksum/test_ip_pseudo_2471_unit.py index 654a2a8931..cb2f8ae8c3 100644 --- a/tests/protocols/misc/checksum/test_ip_pseudo_2471_unit.py +++ b/tests/protocols/misc/checksum/test_ip_pseudo_2471_unit.py @@ -124,21 +124,27 @@ def test_explicit_ipv4(self) -> None: self.assertEqual(pseudo.protocol, TransType.UDP) def test_explicit_ipv6(self) -> None: + from pcapkit.const.reg.transtype import TransType from pcapkit.protocols.misc.checksum.ip_pseudo import IP_Pseudo - pseudo = IP_Pseudo(src=self.SRC6, dst=self.DST6, protocol='TCP', upper_length=0x10203) + pseudo = IP_Pseudo(src=self.SRC6, dst=self.DST6, protocol=TransType.TCP, upper_length=0x10203) self.assertEqual(pseudo.data, self.ADDRS6 + bytes.fromhex('00010203' '000000' '06')) self.assertEqual(pseudo.length, 40) self.assertEqual(pseudo.src, ipaddress.IPv6Address(self.SRC6)) self.assertEqual(pseudo.dst, ipaddress.IPv6Address(self.DST6)) def test_protocol_namespace(self) -> None: + """#2594: a name in a namespace of its own resolves first, and ``make`` refuses the name.""" from pcapkit.protocols.misc.checksum.ip_pseudo import IP_Pseudo + from pcapkit.utilities.exceptions import ProtocolError - pseudo = IP_Pseudo(src='10.0.0.1', dst='10.0.0.2', protocol='spam', - protocol_namespace={99: 'spam'}, upper_length=0) + code = IP_Pseudo.resolve_index('spam', namespace={99: 'spam'}) + self.assertEqual(code, 99) + pseudo = IP_Pseudo(src='10.0.0.1', dst='10.0.0.2', protocol=code, upper_length=0) self.assertEqual(pseudo.data[9], 99) self.assertEqual(pseudo.read().protocol, 99) + with self.assertRaises(ProtocolError): + IP_Pseudo(src='10.0.0.1', dst='10.0.0.2', protocol='spam', upper_length=0) def test_make_read_round_trip(self) -> None: from pcapkit.protocols.misc.checksum.ip_pseudo import IP_Pseudo @@ -160,10 +166,11 @@ def test_nonzero_reserved_octets_round_trip(self) -> None: self.assertEqual(bytes(IP_Pseudo.from_data(parsed.info)), octets) def test_explicit_values_override_the_layer(self) -> None: - from pcapkit.protocols.misc.checksum.ip_pseudo import IP_Pseudo + from pcapkit.const.reg.transtype import TransType from pcapkit.protocols.internet.ipv4 import IPv4 + from pcapkit.protocols.misc.checksum.ip_pseudo import IP_Pseudo - ip = IPv4(src='10.0.0.1', dst='10.0.0.2', protocol='TCP', payload=bytes(20)) + ip = IPv4(src='10.0.0.1', dst='10.0.0.2', protocol=TransType.TCP, payload=bytes(20)) self.assertEqual(IP_Pseudo(ip=ip).data, bytes.fromhex('0a000001' '0a000002' '00' '06' '0014')) self.assertEqual(IP_Pseudo(ip=ip, dst='10.0.0.3', upper_length=7).data, bytes.fromhex('0a000001' '0a000003' '00' '06' '0007')) diff --git a/tests/protocols/misc/test_capture_message_prefix_2300_2301_unit.py b/tests/protocols/misc/test_capture_message_prefix_2300_2301_unit.py index 9d65a794df..15ff09ce2a 100644 --- a/tests/protocols/misc/test_capture_message_prefix_2300_2301_unit.py +++ b/tests/protocols/misc/test_capture_message_prefix_2300_2301_unit.py @@ -24,15 +24,14 @@ ``must be in Interface Description Block, but found in 6 block.`` rather than ``not Enhanced_Packet_Block``. -The placement guard is reached on both passes, and ``_type`` is not the same -kind of object on each: the parse pass assigns the -:class:`~pcapkit.const.pcapng.block_type.BlockType` off the schema, while the -make pass assigns what :meth:`~pcapkit.protocols.protocol.Protocol._make_index` -returns, a plain :obj:`int` that is only :func:`~typing.cast` to the -enumeration. Reading ``.name`` off it directly therefore raises -:exc:`AttributeError` on the make pass, which is what -:func:`~pcapkit.protocols.misc.pcapng._block_type_name` exists to avoid; the -make-pass cases below are what pin that. +The placement guard is reached on both passes. The parse pass assigns ``_type`` +the :class:`~pcapkit.const.pcapng.block_type.BlockType` off the schema. The make +pass assigns what :meth:`~pcapkit.protocols.protocol.Protocol._make_index` +resolves the code to: the member for a code a member holds, which the make-pass +cases below pin, and the plain :obj:`int` for an in-range code no member holds. +That int has no ``.name``, so +:func:`~pcapkit.protocols.misc.pcapng._block_type_name` looks the code up and +names either. Every case builds its own octets in memory and reads no capture. Classes are imported inside each test, after @@ -154,13 +153,7 @@ def _context(self) -> 'Any': return context def test_placement_guard_names_the_block_type(self) -> None: - """The block type is named, not numbered, and the message has no full stop. - - This runs through :meth:`PCAPNG.make`, where ``_type`` is a plain - :obj:`int`, so it is also what pins the guard against the - :exc:`AttributeError` a direct ``.name`` would raise there. - - """ + """The block type is named, not numbered, and the message has no full stop.""" from pcapkit.const.pcapng.block_type import BlockType from pcapkit.const.pcapng.option_type import OptionType from pcapkit.protocols.misc.pcapng import PCAPNG @@ -176,7 +169,7 @@ def test_placement_guard_names_the_block_type(self) -> None: 'Block, not Interface_Statistics_Block') def test_placement_guard_does_not_raise_attribute_error(self) -> None: - """The make pass's plain-``int`` ``_type`` reaches the guard, not an AttributeError.""" + """The make pass's ``_type`` reaches the guard as the member, not an AttributeError (#2594).""" from pcapkit.const.pcapng.block_type import BlockType from pcapkit.const.pcapng.option_type import OptionType from pcapkit.protocols.misc.pcapng import PCAPNG @@ -199,18 +192,18 @@ def spy(inner_self: 'Any', *args: 'Any', **kwargs: 'Any') -> 'Any': finally: PCAPNG._make_option_if_name = original # type: ignore[method-assign] - # The guard ran with a plain ``int``, which has no ``.name``. - self.assertEqual(seen['class'], 'int') + # The guard ran with the member ``_make_index`` resolved, as on the parse pass. + self.assertEqual(seen['class'], 'BlockType') def test_block_type_name_takes_both_an_enum_and_a_plain_int(self) -> None: - """The helper covers both passes' idea of ``_type``. - - The parse pass assigns the enumeration member off the schema and the - make pass a plain :obj:`int`, and the guard is shared, so the helper - has to name either. Reaching a placement guard through a *parse* is - not possible -- an option code is resolved in the namespace of the - block being parsed, so code 2 in an Interface Statistics Block is - ``isb_starttime`` and never ``if_name`` -- which is why this is + """The helper names a member and its plain :obj:`int` code alike. + + The make pass keeps an in-range code no member holds as a plain + :obj:`int`, and the guard is shared with the parse pass, so the helper + names a member and a code alike. Reaching a placement guard through a + *parse* is not possible -- an option code is resolved in the namespace + of the block being parsed, so code 2 in an Interface Statistics Block + is ``isb_starttime`` and never ``if_name`` -- which is why this is checked on the helper rather than through a capture. """ @@ -219,10 +212,24 @@ def test_block_type_name_takes_both_an_enum_and_a_plain_int(self) -> None: self.assertEqual(_block_type_name(BlockType.Enhanced_Packet_Block), 'Enhanced_Packet_Block') - self.assertEqual(_block_type_name(6), 'Enhanced_Packet_Block') # type: ignore[arg-type] + self.assertEqual(_block_type_name(6), 'Enhanced_Packet_Block') self.assertEqual(_block_type_name(BlockType.Interface_Description_Block), 'Interface_Description_Block') - self.assertEqual(_block_type_name(1), 'Interface_Description_Block') # type: ignore[arg-type] + self.assertEqual(_block_type_name(1), 'Interface_Description_Block') + + def test_block_type_name_names_a_code_no_member_holds(self) -> None: + """#2594: ``make`` keeps an in-range code no member holds as a plain :obj:`int`. + + ``0x100`` sits below every range ``BlockType``'s ``_missing_`` resolves, + so the lookup has no member to name and the helper names the code. + + """ + from pcapkit.const.pcapng.block_type import BlockType + from pcapkit.protocols.misc.pcapng import _block_type_name + + with self.assertRaises(ValueError): + BlockType(0x100) + self.assertEqual(_block_type_name(0x100), 'Block 256') def test_only_one_guard_has_no_full_stop(self) -> None: """A repeated "only one" option reports the count in the house form.""" diff --git a/tests/protocols/misc/test_misc_make_codes_2594_unit.py b/tests/protocols/misc/test_misc_make_codes_2594_unit.py new file mode 100644 index 0000000000..fcd1d2b191 --- /dev/null +++ b/tests/protocols/misc/test_misc_make_codes_2594_unit.py @@ -0,0 +1,284 @@ +# -*- coding: utf-8 -*- +"""GitHub issue #2594: the enumeration fields of the misc makers take a member or a code. + +Ruled in review of #2593 (#2489) and on #2594: a ``make()`` field holding an +enumeration takes the member or an :class:`int`, resolved with the +enumeration's ``get``, and no maker takes an ``X_default``, ``X_namespace`` or +``X_reversed`` argument for it; a caller holding a name resolves it with +:meth:`~pcapkit.protocols.protocol.Protocol.resolve_index` first. Under #2617 a +code the field cannot carry raises +:exc:`~pcapkit.utilities.exceptions.ProtocolError`, and so does a :obj:`bool` +for a field wider than one bit. + +:data:`ROWS` holds one row per migrated field of +:class:`~pcapkit.protocols.misc.pcapng.PCAPNG`, +:class:`~pcapkit.protocols.misc.pcap.header.Header` and +:class:`~pcapkit.protocols.misc.checksum.ip_pseudo.IP_Pseudo`: the maker, the +field, its enumeration, its width in bits as the schema declares it, the code +of a member, and the prefix of the refusal's message. The code of each option +and the type of each name resolution record given to a block maker, as a +``(code, args)`` tuple, are such fields too. Each row is checked for: + +* the widest code building, and a negative code and ``1 << bits`` raising + :exc:`~pcapkit.utilities.exceptions.ProtocolError`; +* :data:`True` raising it, every field here being wider than one bit; +* the member and ``int(member)`` building identical octets, and the member's + name, as a :obj:`str`, raising it. + +A code the field carries that no member holds is not refused: the resolver +keeps it as a plain :obj:`int`, so a block type no member holds is written as +given and a parse of it rebuilds byte-exact. + +An option maker is reached through its block's ``options``, as a caller +reaches it. Every case builds its own octets in memory and reads no capture. +Classes are imported inside each test, after +:func:`~tests._support.reimport_once_per_class`. + +""" + +import importlib +import inspect +import unittest +import warnings +from typing import TYPE_CHECKING + +from tests._support import reimport_once_per_class + +if TYPE_CHECKING: + from typing import Any, Callable + +#: One row per migrated field: the maker, the field, the module and name of its +#: enumeration, its width in bits, the code of a member, and the message prefix. +ROWS = ( + ('PCAPNG.make', 'type', 'pcapkit.const.pcapng.block_type', 'BlockType', 32, + 0x0A0D0D0A, 'PCAP-NG: invalid block type: '), + ('PCAPNG._make_block_idb', 'linktype', 'pcapkit.const.reg.linktype', 'LinkType', 16, + 1, 'PCAP-NG: invalid [IDB] linktype: '), + ('PCAPNG._make_block_dsb', 'secrets_type', 'pcapkit.const.pcapng.secrets_type', 'SecretsType', 32, + 0x5A4E574B, 'PCAP-NG: invalid [DSB] secrets_type: '), + ('PCAPNG._make_option_if_filter', 'filter', 'pcapkit.const.pcapng.filter_type', 'FilterType', 8, + 1, 'PCAP-NG: invalid [if_filter] filter: '), + ('PCAPNG._make_option_epb_flags', 'direction', 'pcapkit.protocols.misc.pcapng', 'PacketDirection', 2, + 1, 'PCAP-NG: invalid [epb_flags] direction: '), + ('PCAPNG._make_option_epb_flags', 'reception', 'pcapkit.protocols.misc.pcapng', 'PacketReception', 3, + 2, 'PCAP-NG: invalid [epb_flags] reception: '), + ('PCAPNG._make_option_epb_hash', 'algorithm', 'pcapkit.const.pcapng.hash_algorithm', 'HashAlgorithm', 8, + 2, 'PCAP-NG: invalid [epb_hash] algorithm: '), + ('PCAPNG._make_option_epb_verdict', 'verdict', 'pcapkit.const.pcapng.verdict_type', 'VerdictType', 8, + 1, 'PCAP-NG: invalid [epb_verdict] verdict: '), + ('PCAPNG._make_option_pack_flags', 'direction', 'pcapkit.protocols.misc.pcapng', 'PacketDirection', 2, + 2, 'PCAP-NG: invalid [pack_flags] direction: '), + ('PCAPNG._make_option_pack_flags', 'reception', 'pcapkit.protocols.misc.pcapng', 'PacketReception', 3, + 3, 'PCAP-NG: invalid [pack_flags] reception: '), + ('PCAPNG._make_option_pack_hash', 'algorithm', 'pcapkit.const.pcapng.hash_algorithm', 'HashAlgorithm', 8, + 2, 'PCAP-NG: invalid [pack_hash] algorithm: '), + ('Header.make', 'network', 'pcapkit.const.reg.linktype', 'LinkType', 32, + 1, 'PCAP: invalid network: '), + ('IP_Pseudo.make', 'protocol', 'pcapkit.const.reg.transtype', 'TransType', 8, + 17, 'IP_Pseudo: invalid protocol: '), + ('PCAPNG._make_pcapng_options', 'code', 'pcapkit.const.pcapng.option_type', 'OptionType', 16, + 1, 'PCAP-NG: invalid option code: '), + ('PCAPNG._make_nrb_records', 'type', 'pcapkit.const.pcapng.record_type', 'RecordType', 16, + 1, 'PCAP-NG: invalid record type: '), +) + +#: The arguments of an IPv4 name resolution record. +IPV4_RECORD = {'ip': '10.0.0.1', 'names': ['host']} + +#: Timestamp and packet data of the packet blocks, fixed so that two builds compare. +TIMESTAMP = 1500000000 +PACKET = bytes(16) + + +class MiscMakeCodeTests(unittest.TestCase): + """Each migrated field takes a member or a code its width carries, and nothing else.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def _context(self) -> 'Any': + """A one-section context with one Ethernet interface, as the engine builds it.""" + from pcapkit.const.pcapng.block_type import BlockType + from pcapkit.const.reg.linktype import LinkType + from pcapkit.foundation.engines.pcapng import Context + from pcapkit.protocols.misc.pcapng import PCAPNG + + section = PCAPNG(num=0, sct=1, ctx=None, type=BlockType.Section_Header_Block, block={}) + context = Context(section.info) # type: Any + interface = PCAPNG(num=1, sct=1, ctx=context, type=BlockType.Interface_Description_Block, + block={'linktype': LinkType.ETHERNET, 'snaplen': 0x40000}) + context.interfaces.append(interface.info) + return context + + def _block(self, name: 'str', block: 'dict[str, Any]') -> 'bytes': + """The octets of the block named ``name``, built from ``block`` in a section.""" + from pcapkit.const.pcapng.block_type import BlockType + from pcapkit.protocols.misc.pcapng import PCAPNG + + with warnings.catch_warnings(): + warnings.simplefilter('ignore') # the Packet Block is obsolete + return PCAPNG(num=2, sct=1, ctx=self._context(), type=BlockType[name], block=block).data + + def _option(self, name: 'str', option: 'str', args: 'dict[str, Any]') -> 'bytes': + """The octets of the block named ``name`` carrying the option ``option`` made from ``args``.""" + from pcapkit.const.pcapng.option_type import OptionType + + block = {'options': [(OptionType[option], args)]} # type: dict[str, Any] + if name == 'Interface_Description_Block': + block.update(linktype=1, snaplen=0x40000) + else: + block.update(timestamp=TIMESTAMP, packet_data=PACKET) + return self._block(name, block) + + def _builder(self, maker: 'str', field: 'str') -> 'Callable[[Any], bytes]': + """How a caller builds octets with ``value`` for ``field`` of ``maker``.""" + from pcapkit.protocols.misc.checksum.ip_pseudo import IP_Pseudo + from pcapkit.protocols.misc.pcap.header import Header + from pcapkit.protocols.misc.pcapng import PCAPNG + + builders = { + 'PCAPNG.make': lambda value: PCAPNG(num=0, sct=1, ctx=None, type=value, block={}).data, + 'PCAPNG._make_block_idb': lambda value: self._block( + 'Interface_Description_Block', {'linktype': value, 'snaplen': 0x40000}), + 'PCAPNG._make_block_dsb': lambda value: self._block( + 'Decryption_Secrets_Block', + {'secrets_type': value, 'secrets_data': {'nwk_key': bytes(16), 'pan_id': 0x1234}}), + 'PCAPNG._make_option_if_filter': lambda value: self._option( + 'Interface_Description_Block', 'if_filter', {'filter': value, 'expression': b'udp'}), + 'PCAPNG._make_option_epb_flags': lambda value: self._option( + 'Enhanced_Packet_Block', 'epb_flags', {field: value}), + 'PCAPNG._make_option_epb_hash': lambda value: self._option( + 'Enhanced_Packet_Block', 'epb_hash', {'algorithm': value, 'hash': b'\x01\x02\x03\x04'}), + 'PCAPNG._make_option_epb_verdict': lambda value: self._option( + 'Enhanced_Packet_Block', 'epb_verdict', {'verdict': value, 'value': b'\x01'}), + 'PCAPNG._make_option_pack_flags': lambda value: self._option( + 'Packet_Block', 'pack_flags', {field: value}), + 'PCAPNG._make_option_pack_hash': lambda value: self._option( + 'Packet_Block', 'pack_hash', {'algorithm': value, 'hash': b'\x01\x02\x03\x04'}), + 'Header.make': lambda value: Header(network=value, byteorder='little').data, + 'IP_Pseudo.make': lambda value: IP_Pseudo(src='10.0.0.1', dst='10.0.0.2', protocol=value, + upper_length=0).data, + 'PCAPNG._make_pcapng_options': lambda value: self._block( + 'Interface_Description_Block', + {'linktype': 1, 'snaplen': 0x40000, 'options': [(value, {'comment': 'eth0'})]}), + 'PCAPNG._make_nrb_records': lambda value: self._block( + 'Name_Resolution_Block', {'records': [(value, IPV4_RECORD)]}), + } # type: dict[str, Callable[[Any], bytes]] + return builders[maker] + + def _rows(self) -> 'list[tuple[str, int, Any, str, Callable[[Any], bytes]]]': + """Each row as its label, its width, its member, its message prefix and its builder.""" + rows = [] + for maker, field, module, enum, bits, code, prefix in ROWS: + member = getattr(importlib.import_module(module), enum).get(code) + rows.append((f'{maker} {field}', bits, member, prefix, self._builder(maker, field))) + return rows + + def assertRefused(self, build: 'Callable[[Any], bytes]', value: 'Any', prefix: 'str') -> None: # pylint: disable=invalid-name + """Building with ``value`` raises the field's own ProtocolError, naming ``value``.""" + from pcapkit.utilities.exceptions import ProtocolError + + try: + build(value) + except ProtocolError as error: + self.assertEqual(str(error), f'{prefix}{value!r}') + return + except Exception as error: # pylint: disable=broad-except + self.fail(f'{value!r} raised {type(error).__name__}, not ProtocolError: {error}') + self.fail(f'{value!r} was accepted') + + def test_a_code_past_the_field_is_refused(self) -> None: + for label, bits, _, prefix, build in self._rows(): + with self.subTest(field=label): + self.assertRefused(build, -1, prefix) + self.assertRefused(build, 1 << bits, prefix) + build((1 << bits) - 1) + + def test_a_bool_is_refused(self) -> None: + for label, bits, _, prefix, build in self._rows(): + with self.subTest(field=label): + self.assertGreater(bits, 1) + self.assertRefused(build, True, prefix) + + def test_a_member_and_its_code_build_alike_and_its_name_is_refused(self) -> None: + for label, _, member, prefix, build in self._rows(): + with self.subTest(field=label): + self.assertRefused(build, member.name, prefix) + self.assertEqual(build(member).hex(), build(int(member)).hex()) + + def test_a_block_type_no_member_holds_is_written_as_given(self) -> None: + """An in-range code no member holds builds, and a parse of it rebuilds byte-exact. + + ``0x100`` sits below every range ``BlockType``'s ``_missing_`` resolves, + so ``get`` misses and ``make`` keeps the code as a plain :obj:`int`. + + """ + from pcapkit.const.pcapng.block_type import BlockType + from pcapkit.protocols.misc.pcapng import PCAPNG + + with self.assertRaises(ValueError): + BlockType.get(0x100) + octets = PCAPNG(num=0, sct=1, ctx=None, type=0x100, block={'data': b'ab'}).data + self.assertEqual(octets.hex(), '00010000100000006162000010000000') + parsed = PCAPNG(octets, len(octets), num=0, sct=1, ctx=None) + self.assertEqual(PCAPNG.from_data(parsed.info, num=0, sct=1, ctx=None).data.hex(), octets.hex()) + + def test_an_option_code_resolves_in_its_blocks_namespace(self) -> None: + """An option code is looked up in the namespace of the block it is given to, as a parse reads it.""" + from pcapkit.const.pcapng.option_type import OptionType + + self.assertEqual(self._option('Interface_Description_Block', 'if_name', {'name': 'eth0'}).hex(), + self._block('Interface_Description_Block', + {'linktype': 1, 'snaplen': 0x40000, + 'options': [(2, {'name': 'eth0'})]}).hex()) + self.assertEqual(self._block('Interface_Statistics_Block', + {'interface_id': 0, 'timestamp': TIMESTAMP, + 'options': [(OptionType.isb_starttime, {'timestamp': TIMESTAMP})]}).hex(), + self._block('Interface_Statistics_Block', + {'interface_id': 0, 'timestamp': TIMESTAMP, + 'options': [(2, {'timestamp': TIMESTAMP})]}).hex()) + + def test_a_parsed_option_or_record_list_has_its_keys_checked(self) -> None: + """A parsed list rebuilds as it was, and one keyed by a refused code is refused.""" + from pcapkit.const.pcapng.option_type import OptionType + from pcapkit.const.pcapng.record_type import RecordType + from pcapkit.corekit.multidict import OrderedMultiDict + from pcapkit.protocols.misc.pcapng import PCAPNG + + cases = ( + ('Interface_Description_Block', {'linktype': 1, 'snaplen': 0x40000}, 'options', + OptionType.if_name, {'name': 'eth0'}, 16, 'PCAP-NG: invalid option code: '), + ('Name_Resolution_Block', {}, 'records', + RecordType.nrb_record_ipv4, IPV4_RECORD, 16, 'PCAP-NG: invalid record type: '), + ) # type: tuple[tuple[str, dict[str, Any], str, Any, dict[str, Any], int, str], ...] + for name, fixed, key, member, args, bits, prefix in cases: + octets = self._block(name, dict(fixed, **{key: [(member, args)]})) + parsed = getattr(PCAPNG(octets, len(octets), num=2, sct=1, ctx=self._context()).info, key) + item = next(iter(parsed.items(multi=True)))[1] + + def build(value: 'Any', name: 'str' = name, fixed: 'dict[str, Any]' = fixed, + key: 'str' = key, item: 'Any' = item) -> bytes: + return self._block(name, dict(fixed, **{key: OrderedMultiDict([(value, item)])})) + + with self.subTest(block=name, list=key): + self.assertEqual(build(member).hex(), octets.hex()) + for value in (1 << bits, -1, True, member.name): + self.assertRefused(build, value, prefix) + + def test_no_maker_takes_the_removed_arguments(self) -> None: + from pcapkit.protocols.misc.checksum.ip_pseudo import IP_Pseudo + from pcapkit.protocols.misc.pcap.header import Header + from pcapkit.protocols.misc.pcapng import PCAPNG + + for cls in (PCAPNG, Header, IP_Pseudo): + for name in vars(cls): + if name != 'make' and not name.startswith('_make_'): + continue + with self.subTest(maker=f'{cls.__name__}.{name}'): + removed = [param for param in inspect.signature(getattr(cls, name)).parameters + if param.endswith(('_default', '_namespace', '_reversed'))] + self.assertEqual(removed, []) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/protocols/test_make_index_str_unit.py b/tests/protocols/test_make_index_str_unit.py index e05cd41234..6e60a88810 100644 --- a/tests/protocols/test_make_index_str_unit.py +++ b/tests/protocols/test_make_index_str_unit.py @@ -1,19 +1,21 @@ # -*- coding: utf-8 -*- -"""``make()`` resolves an enum member name given as :obj:`str`. - -GitHub issue #1210: the ``make()`` signatures declare enum arguments as -``Enum | StdlibEnum | AenumEnum | str | int``, but without the fix -:meth:`~pcapkit.protocols.protocol.ProtocolBase._make_index` raises -:exc:`~pcapkit.utilities.exceptions.ProtocolNotImplemented` for a :obj:`str` -unless a ``*_namespace`` is passed. A member name resolves against the -enumeration class of the argument's default value, and builds the same bytes -as the member itself and as its integer value. - -GitHub issue #2356 is the other half: an argument carrying an optional field -defaults to :data:`None`, which is no enumeration member, so there is nothing -for that inference to read. Such a maker names its enumeration at the -:meth:`~pcapkit.protocols.protocol.ProtocolBase._make_index` call instead, and -:data:`NONE_DEFAULT_CASES` carries one case per argument that does. +"""``make()`` refuses a member name, and ``resolve_index`` resolves one. + +GitHub issue #2594. The owner ruled, in review of #2593 (which closes #2489), that a +``make()`` field holding an enumeration takes the member or an :class:`int`, +resolved with the enumeration's ``get``, and that nothing inside ``make()`` turns a +:obj:`str` into a member. Each case below is a field a name could be inferred for +through the calling frame, from the argument's own default (#1210) or from an +enumeration the maker named where that default is :data:`None` (#2356). Each pins +both halves of the rule: the name is refused with +:exc:`~pcapkit.utilities.exceptions.ProtocolError`, and +:meth:`~pcapkit.protocols.protocol.ProtocolBase.resolve_index` turns it into what +``make()`` takes, building the same octets as the member and as its integer value. + +:class:`TestMakeIndex` and :class:`TestResolveIndex` pin the two helpers themselves: +the range check :meth:`~pcapkit.protocols.protocol.ProtocolBase._make_index` carries +for every caller (#2617), and the keyword group taken explicitly. +:class:`TestMakePort` pins the same check on the transport ports. Every case builds its own packet in memory and reads no capture. Classes are imported inside each test, after @@ -65,30 +67,24 @@ def build(**kwargs: 'Any') -> 'Any': return build -#: One ``make()`` per protocol family: -#: (protocol class or builder, argument, enum class, non-default member). +#: One ``make()`` per protocol family, each an argument whose default is a member: +#: (protocol class or builder, argument, enum class, non-default member, the +#: other keywords the maker needs). CASES = { 'link/ethernet': ('pcapkit.protocols.link.ethernet.Ethernet', 'type', - 'pcapkit.const.reg.ethertype.EtherType', 'Internet_Protocol_version_6'), + 'pcapkit.const.reg.ethertype.EtherType', 'Internet_Protocol_version_6', {}), 'link/l2tpv2': ('pcapkit.protocols.link.l2tpv2.L2TPv2', 'type', - 'pcapkit.const.l2tp.type.Type', 'Control'), + 'pcapkit.const.l2tp.type.Type', 'Control', {}), 'internet/ipv4': ('pcapkit.protocols.internet.ipv4.IPv4', 'protocol', - 'pcapkit.const.reg.transtype.TransType', 'TCP'), + 'pcapkit.const.reg.transtype.TransType', 'TCP', {}), 'transport/tcp': (_tcp_alternate_checksum, 'algorithm', - 'pcapkit.const.tcp.checksum.Checksum', 'Checksum_16_bit_Fletcher_s_algorithm'), + 'pcapkit.const.tcp.checksum.Checksum', 'Checksum_16_bit_Fletcher_s_algorithm', {}), 'application/ospf': ('pcapkit.protocols.application.ospf.OSPF', 'type', - 'pcapkit.const.ospf.packet.Packet', 'Database_Description'), + 'pcapkit.const.ospf.packet.Packet', 'Database_Description', {}), 'misc/pcap': ('pcapkit.protocols.misc.pcap.header.Header', 'network', - 'pcapkit.const.reg.linktype.LinkType', 'RAW'), -} - - -#: One ``make()`` per argument whose own default is :data:`None`, so that no enum -#: default is there for the namespace to be inferred from (#2356): (builder, -#: argument, enum class, member, the other keywords the option needs). Each -#: member is one the maker would not have arrived at on its own, so a name that -#: went unresolved cannot pass as a name that resolved. -NONE_DEFAULT_CASES = { + 'pcapkit.const.reg.linktype.LinkType', 'RAW', {}), + # arguments defaulting to None, whose maker named the enumeration at the call + # for a name to resolve against (#2356) 'internet/hopopt MPL': (_ipv6_option('pcapkit.protocols.internet.hopopt.HOPOPT', 'MPL_Option'), 'seed_type', 'pcapkit.const.ipv6.seed_id.SeedID', 'SEEDID_64_BIT_UNSIGNED_INTEGER', {'seed': 5, 'seq': 7}), @@ -104,138 +100,203 @@ def build(**kwargs: 'Any') -> 'Any': } -class TestMakeIndexStr(unittest.TestCase): - """Pin member-name resolution in ``_make_index`` across families.""" +class TestMakeRefusesNames(unittest.TestCase): + """A member name given to ``make()`` is refused; ``resolve_index`` resolves it.""" def setUp(self) -> None: reimport_once_per_class(self) - def test_member_name_matches_member_and_int(self) -> None: - for case, (maker, arg, enum_path, member) in CASES.items(): + def test_member_name_is_refused(self) -> None: + exc = _attr('pcapkit.utilities.exceptions.ProtocolError') + for case, (maker, arg, _, member, extra) in CASES.items(): with self.subTest(case=case): build = _attr(maker) if isinstance(maker, str) else maker - value = getattr(_attr(enum_path), member) - - expected = build(**{arg: value}).data - self.assertEqual(build(**{arg: member}).data, expected) - self.assertEqual(build(**{arg: int(value)}).data, expected) - # the member is not the default, so the name was not ignored - self.assertNotEqual(build().data, expected) - - def test_member_name_resolves_where_the_argument_defaults_to_none(self) -> None: - """An argument whose own default is :data:`None` resolves a name too. See #2356. - - The namespace is inferred from the enumeration class of the argument's - default value, which an argument defaulting to :data:`None` -- because - the field it carries is optional -- does not have. Such a maker names - its enumeration at the call instead, so that a member name resolves - without the caller also passing a ``*_namespace``. - """ - for case, (build, arg, enum_path, member, extra) in NONE_DEFAULT_CASES.items(): + with self.assertRaises(exc): + build(**extra, **{arg: member}) + + def test_resolved_name_matches_member_and_int(self) -> None: + protocol = _attr('pcapkit.protocols.protocol.Protocol') + for case, (maker, arg, enum_path, member, extra) in CASES.items(): with self.subTest(case=case): - value = getattr(_attr(enum_path), member) + build = _attr(maker) if isinstance(maker, str) else maker + namespace = _attr(enum_path) + value = getattr(namespace, member) expected = build(**extra, **{arg: value}).data - self.assertEqual(build(**extra, **{arg: member}).data, expected) + resolved = protocol.resolve_index(member, namespace=namespace) + self.assertIs(resolved, value) + self.assertEqual(build(**extra, **{arg: resolved}).data, expected) self.assertEqual(build(**extra, **{arg: int(value)}).data, expected) - # the maker derives the field from its siblings when the argument - # is not given, and derives a different value here + # the member is not what the maker builds on its own self.assertNotEqual(build(**extra).data, expected) - def test_http_status_member_name_resolves(self) -> None: - """An HTTP/1.* status code resolves from its member name. See #2356. - - ``status`` defaults to :data:`None`, since a response has a status where - a request has a method and a URI, so its namespace is named at the call. - :meth:`~pcapkit.protocols.protocol.ProtocolBase._make_index` yields the - member's integer, and a start line built from the integer carries no - reason phrase; only the member itself brings one along. - """ + def test_http_status_name_is_refused(self) -> None: + """``status`` defaults to :data:`None`, as a request carries none (#2356).""" + exc = _attr('pcapkit.utilities.exceptions.ProtocolError') http = _attr('pcapkit.protocols.application.httpv1.HTTPv1') status_code = _attr('pcapkit.const.http.status_code.StatusCode') + protocol = _attr('pcapkit.protocols.protocol.Protocol') - self.assertEqual(http(status='CODE_404').data, http(status=404).data) - self.assertEqual(http(status='CODE_404').data, b'HTTP/1.1 404 \r\n\r\n') - self.assertEqual(http(status=status_code.CODE_404).data, b'HTTP/1.1 404 Not Found\r\n\r\n') + with self.assertRaises(exc): + http(status='CODE_404') + resolved = protocol.resolve_index('CODE_404', namespace=status_code) + self.assertEqual(http(status=resolved).data, http(status=status_code.CODE_404).data) - def test_unknown_name_still_raises(self) -> None: - exc = _attr('pcapkit.utilities.exceptions.ProtocolNotImplemented') + def test_removed_keywords_are_refused(self) -> None: + exc = _attr('pcapkit.utilities.exceptions.UnsupportedCall') ethernet = _attr('pcapkit.protocols.link.ethernet.Ethernet') - with self.assertRaises(exc): - ethernet(type='No_Such_EtherType') + for keyword, value in (('type_default', 0x1234), ('type_namespace', {0x1234: 'X'}), + ('type_reversed', True)): + with self.subTest(keyword=keyword): + with self.assertRaises(exc): + ethernet(**{keyword: value}) - # a named namespace resolves a member name, not an arbitrary string: an - # HTTP status code member is spelled ``CODE_404``, so ``'OK'`` is none - http = _attr('pcapkit.protocols.application.httpv1.HTTPv1') - with self.assertRaises(exc): - http(status='OK') - def test_unknown_name_falls_back_to_default(self) -> None: - ethernet = _attr('pcapkit.protocols.link.ethernet.Ethernet') - proto = ethernet(type='No_Such_EtherType', type_default=0x1234) - self.assertEqual(proto.data[12:14], b'\x12\x34') +class TestMakeIndex(unittest.TestCase): + """``_make_index`` resolves a member or a code, range-checked (#2594, #2617).""" - def test_explicit_namespace_wins(self) -> None: - ethernet = _attr('pcapkit.protocols.link.ethernet.Ethernet') - # the name is also an EtherType member, but the namespace decides - proto = ethernet(type='Internet_Protocol_version_6', - type_namespace={'Internet_Protocol_version_6': 0x1234}, - type_reversed=True) - self.assertEqual(proto.data[12:14], b'\x12\x34') - - def test_shared_name_with_one_meaning_resolves(self) -> None: - ipv4 = _attr('pcapkit.protocols.internet.ipv4.IPv4') + def setUp(self) -> None: + reimport_once_per_class(self) + + def _raw(self) -> 'Any': + return _attr('pcapkit.protocols.misc.raw.Raw')(b'') + + def test_member_and_code_resolve_to_the_member(self) -> None: + transtype = _attr('pcapkit.const.reg.transtype.TransType') + raw = self._raw() + self.assertIs(raw._make_index(transtype.TCP, transtype, bits=8, name='protocol'), transtype.TCP) + self.assertIs(raw._make_index(6, transtype, bits=8, name='protocol'), transtype.TCP) + self.assertIs(raw._make_index(255, transtype, bits=8, name='protocol'), transtype(255)) + + def test_a_member_of_another_enumeration_resolves_by_value(self) -> None: + transtype = _attr('pcapkit.const.reg.transtype.TransType') + ethertype = _attr('pcapkit.const.reg.ethertype.EtherType') + member = ethertype.get(6) + self.assertIs(self._raw()._make_index(member, transtype, bits=8, name='protocol'), transtype.TCP) + + def test_refusals(self) -> None: + exc = _attr('pcapkit.utilities.exceptions.ProtocolError') + transtype = _attr('pcapkit.const.reg.transtype.TransType') + raw = self._raw() + for value in ('TCP', 'tcp', b'\x06', True, False, 256, -1, 6.0, None): + with self.subTest(value=value): + with self.assertRaisesRegex(exc, r'^Raw: invalid protocol: '): + raw._make_index(value, transtype, bits=8, name='protocol') + + def test_bool_passes_a_one_bit_field(self) -> None: tos_del = _attr('pcapkit.const.ipv4.tos_del.ToSDelay') - tos_thr = _attr('pcapkit.const.ipv4.tos_thr.ToSThroughput') - tos_rel = _attr('pcapkit.const.ipv4.tos_rel.ToSReliability') + raw = self._raw() + self.assertIs(raw._make_index(True, tos_del, bits=1, name='delay'), tos_del(1)) + self.assertIs(raw._make_index(False, tos_del, bits=1, name='delay'), tos_del(0)) + + def test_the_field_width_decides_the_refusal(self) -> None: + # the field's ten bits decide, not StatusCode's own 0-999 range: a code + # the field carries and the enumeration refuses comes back as the int + exc = _attr('pcapkit.utilities.exceptions.ProtocolError') + status_code = _attr('pcapkit.const.http.status_code.StatusCode') + raw = self._raw() + self.assertIs(raw._make_index(404, status_code, bits=10, name='status'), status_code.CODE_404) + # an unassigned code resolves to the member the registry builds for it + self.assertIsInstance(raw._make_index(999, status_code, bits=10, name='status'), status_code) + code = raw._make_index(1023, status_code, bits=10, name='status') + self.assertIs(type(code), int) + self.assertEqual(code, 1023) + with self.assertRaisesRegex(exc, r'^Raw: invalid status: 1024$'): + raw._make_index(1024, status_code, bits=10, name='status') + + def test_a_closed_enumeration_keeps_a_code_its_field_carries(self) -> None: + # HIP's Transport mode IDs are 0 to 3 in a 16-bit field (#2692 review) + transport = _attr('pcapkit.const.hip.transport.Transport') + raw = self._raw() + with self.assertRaises(ValueError): + transport.get(1000) + code = raw._make_index(1000, transport, bits=16, name='mode ID') + self.assertIs(type(code), int) + self.assertEqual(code, 1000) + + def test_a_code_no_member_holds_is_kept(self) -> None: + # in EtherType's range but in no member's, as an unassigned code a parse + # keeps: it is written as given, so the capture rebuilds as it was + ethertype = _attr('pcapkit.const.reg.ethertype.EtherType') + raw = self._raw() + with self.assertRaises(ValueError): + ethertype.get(0x1234) + code = raw._make_index(0x1234, ethertype, bits=16, name='EtherType') + self.assertIs(type(code), int) + self.assertEqual(code, 0x1234) + + def test_alias_prefixes_the_message(self) -> None: + exc = _attr('pcapkit.utilities.exceptions.ProtocolError') + transtype = _attr('pcapkit.const.reg.transtype.TransType') + with self.assertRaisesRegex(exc, r'^IPv4: invalid protocol: 256$'): + self._raw()._make_index(256, transtype, bits=8, name='protocol', alias='IPv4') - expected = ipv4(tos_del=tos_del.NORMAL, tos_thr=tos_thr.NORMAL).data - self.assertEqual(ipv4(tos_del='NORMAL', tos_thr='NORMAL').data, expected) - # an equal but distinct string object, so the match is not by identity - self.assertEqual(ipv4(tos_del='NORMAL', tos_thr=''.join(['NOR', 'MAL'])).data, expected) - expected = ipv4(tos_thr=tos_thr.HIGH, tos_rel=tos_rel.HIGH).data - self.assertEqual(ipv4(tos_thr='HIGH', tos_rel='HIGH').data, expected) - self.assertNotEqual(ipv4().data, expected) +class TestMakePort(unittest.TestCase): + """A port the 16-bit field cannot carry raises ``ProtocolError`` (#2594, #2617).""" - def test_shared_name_with_two_meanings_resolves_per_argument(self) -> None: - # the per-argument resolution of #1308; see test_protocol_make_unit - enum = importlib.import_module('enum') - exc = _attr('pcapkit.utilities.exceptions.ProtocolNotImplemented') - protocol = _attr('pcapkit.protocols.protocol.ProtocolBase') + def setUp(self) -> None: + reimport_once_per_class(self) - left = enum.IntEnum('Left', {'Shared': 1}) - right = enum.IntEnum('Right', {'Shared': 2}) + def test_port_refusals(self) -> None: + exc = _attr('pcapkit.utilities.exceptions.ProtocolError') + for path in ('pcapkit.protocols.transport.tcp.TCP', 'pcapkit.protocols.transport.udp.UDP'): + protocol = _attr(path) + for keyword in ('srcport', 'dstport'): + for value in (1 << 16, -1, 'http', True): + with self.subTest(protocol=protocol.__name__, keyword=keyword, value=value): + with self.assertRaisesRegex(exc, rf'^{protocol.__name__}: invalid port: '): + protocol(**{keyword: value}) - class Pair(protocol): # type: ignore[misc,valid-type] - def make_pair(self, a: 'Any' = left.Shared, a_namespace: 'Any' = None, - b: 'Any' = right.Shared, b_namespace: 'Any' = None) -> 'int': - return self._make_index(a, namespace=a_namespace) + def test_widest_port_builds(self) -> None: + tcp = _attr('pcapkit.protocols.transport.tcp.TCP') + self.assertEqual(tcp(srcport=0xFFFF).data[:2], b'\xff\xff') - self.assertEqual(Pair.make_pair(Pair, a='Shared', b='Shared'), 1) - self.assertEqual(Pair.make_pair(Pair, a='Shared', b='Shared', b_namespace=right), 1) - # a real make(): ``LOW`` is a ToSDelay member but not a ToSThroughput one, - # so ``tos_thr`` alone fails to resolve it - ipv4 = _attr('pcapkit.protocols.internet.ipv4.IPv4') - with self.assertRaisesRegex(exc, r"protocol 'LOW' not implemented"): - ipv4(tos_del='LOW', tos_thr='LOW') +class TestResolveIndex(unittest.TestCase): + """``resolve_index`` takes the name, default, namespace and reversed group explicitly (#2594).""" - def test_without_inferable_enum_still_raises(self) -> None: - exc = _attr('pcapkit.utilities.exceptions.ProtocolNotImplemented') - protocol = _attr('pcapkit.protocols.protocol.ProtocolBase') - # called outside any ``make()``, so there is no argument to infer from - with self.assertRaises(exc): - protocol._make_index('Internet_Protocol_version_6') + def setUp(self) -> None: + reimport_once_per_class(self) - # an argument defaulting to ``None`` carries no enum to infer from, so a - # maker that names none at the call leaves the name unresolvable - class OptionalArgument(protocol): # type: ignore[misc,valid-type] - def make_optional(self, arg: 'Any' = None, arg_namespace: 'Any' = None) -> 'int': - return self._make_index(arg, namespace=arg_namespace) + def test_enumeration_name(self) -> None: + protocol = _attr('pcapkit.protocols.protocol.Protocol') + ethertype = _attr('pcapkit.const.reg.ethertype.EtherType') + self.assertIs(protocol.resolve_index('Address_Resolution_Protocol', namespace=ethertype), + ethertype.Address_Resolution_Protocol) - with self.assertRaises(exc): - OptionalArgument.make_optional(OptionalArgument, arg='Internet_Protocol_version_6') + def test_dict_namespace(self) -> None: + protocol = _attr('pcapkit.protocols.protocol.Protocol') + self.assertEqual(protocol.resolve_index('spam', namespace={99: 'spam'}), 99) + self.assertEqual(protocol.resolve_index('spam', namespace={'spam': 99}, reversed=True), 99) + + def test_member_and_code_pass_through(self) -> None: + protocol = _attr('pcapkit.protocols.protocol.Protocol') + transtype = _attr('pcapkit.const.reg.transtype.TransType') + self.assertIs(protocol.resolve_index(transtype.TCP, namespace=transtype), transtype.TCP) + self.assertEqual(protocol.resolve_index(7, namespace=transtype), 7) + + def test_miss_falls_back_to_default(self) -> None: + protocol = _attr('pcapkit.protocols.protocol.Protocol') + ethertype = _attr('pcapkit.const.reg.ethertype.EtherType') + self.assertEqual(protocol.resolve_index('No_Such_EtherType', 0x1234, namespace=ethertype), 0x1234) + self.assertEqual(protocol.resolve_index('eggs', 7, namespace={99: 'spam'}), 7) + self.assertEqual(protocol.resolve_index('eggs', 7, namespace=object()), 7) # type: ignore[arg-type] + + def test_miss_without_default_raises(self) -> None: + exc = _attr('pcapkit.utilities.exceptions.ProtocolNotImplemented') + protocol = _attr('pcapkit.protocols.protocol.Protocol') + ethertype = _attr('pcapkit.const.reg.ethertype.EtherType') + for namespace in (ethertype, {99: 'spam'}, object()): + with self.subTest(namespace=namespace): + with self.assertRaisesRegex(exc, r"^'eggs' is not a name in "): + protocol.resolve_index('eggs', namespace=namespace) + + def test_inherited_by_every_protocol(self) -> None: + ethernet = _attr('pcapkit.protocols.link.ethernet.Ethernet') + ethertype = _attr('pcapkit.const.reg.ethertype.EtherType') + code = ethernet.resolve_index('Address_Resolution_Protocol', namespace=ethertype) + self.assertEqual(ethernet(type=code).data[12:14], b'\x08\x06') if __name__ == '__main__': diff --git a/tests/protocols/test_protocol_base_unit.py b/tests/protocols/test_protocol_base_unit.py index 20fcb7f93c..9524b62e9e 100644 --- a/tests/protocols/test_protocol_base_unit.py +++ b/tests/protocols/test_protocol_base_unit.py @@ -224,15 +224,22 @@ class Number(enum.IntEnum): with self.assertRaises(StructError): DummyProtocol._make_pack(256, size=1) - self.assertEqual(DummyProtocol._make_index(Number.one), 1) - self.assertEqual(DummyProtocol._make_index(7), 7) - self.assertEqual(DummyProtocol._make_index('one', namespace=Number), 1) - self.assertEqual(DummyProtocol._make_index('tcp', namespace={6: 'tcp'}), 6) - self.assertEqual(DummyProtocol._make_index('tcp', namespace={'tcp': 6}, reversed=True), 6) - self.assertEqual(DummyProtocol._make_index('missing', default=99), 99) - self.assertEqual(DummyProtocol._make_index('one', namespace=Number, pack=True, size=1), b'\x01') + # #2594: _make_index resolves a member or a code and refuses a name, + # which resolve_index looks up from a namespace given explicitly + from pcapkit.const.reg.transtype import TransType + from pcapkit.utilities.exceptions import ProtocolError + self.assertIs(proto._make_index(TransType.TCP, TransType, bits=8, name='protocol'), TransType.TCP) + self.assertIs(proto._make_index(6, TransType, bits=8, name='protocol'), TransType.TCP) + with self.assertRaises(ProtocolError): + proto._make_index('TCP', TransType, bits=8, name='protocol') + self.assertIs(DummyProtocol.resolve_index(Number.one, namespace=Number), Number.one) + self.assertEqual(DummyProtocol.resolve_index(7, namespace=Number), 7) + self.assertIs(DummyProtocol.resolve_index('one', namespace=Number), Number.one) + self.assertEqual(DummyProtocol.resolve_index('tcp', namespace={6: 'tcp'}), 6) + self.assertEqual(DummyProtocol.resolve_index('tcp', namespace={'tcp': 6}, reversed=True), 6) + self.assertEqual(DummyProtocol.resolve_index('missing', 99, namespace=Number), 99) with self.assertRaises(ProtocolNotImplemented): - DummyProtocol._make_index('missing') + DummyProtocol.resolve_index('missing', namespace=Number) def test_read_unpack_short_read_fails_for_every_size(self) -> None: # #2279: a size with no struct format code read a short buffer as a @@ -521,8 +528,6 @@ def test_make_pack_raises_struct_error_for_every_size(self) -> None: with self.subTest(integer=integer, **kwargs): with self.assertRaises(StructError): DummyProtocol._make_pack(integer, **kwargs) - with self.assertRaises(StructError): - DummyProtocol._make_index(2 ** 24, pack=True, size=3) self.assertEqual(DummyProtocol._make_pack(-2, size=3, signed=True), b'\xff\xff\xfe') def test_make_payload_branches(self) -> None: diff --git a/tests/protocols/test_protocol_make_unit.py b/tests/protocols/test_protocol_make_unit.py index 73be3808fc..a30b8d449f 100644 --- a/tests/protocols/test_protocol_make_unit.py +++ b/tests/protocols/test_protocol_make_unit.py @@ -11,11 +11,6 @@ holding raw octets where parsing its packed form yields a schema is replaced by the parsed one. -It also guards against GitHub issue #1308, where a :obj:`str` member name given -to two enum arguments of one ``make()`` raised ``ambiguous member name`` when -only one of the enumerations had it. Each ``_make_index`` call resolves against -the argument it was passed. - Every case builds its own packet in memory and reads no capture. Classes are imported inside each test, after :func:`~tests._support.reimport_once_per_class`, so they belong to the live @@ -25,7 +20,6 @@ import importlib import unittest -import unittest.mock from typing import TYPE_CHECKING from tests._support import reimport_once_per_class @@ -170,85 +164,5 @@ def test_malformed_raw_item_raises_protocol_error(self) -> None: hip(extension=True, parameters=[b'\x00\x41\x00\x08' + bytes(8)]) -class TestMakeIndexPerArgument(unittest.TestCase): - """A member name resolves against its own ``make()`` argument (#1308).""" - - def setUp(self) -> None: - reimport_once_per_class(self) - - def _pair(self) -> 'tuple[Any, Any, Any]': - enum = importlib.import_module('enum') - protocol = _attr('pcapkit.protocols.protocol.ProtocolBase') - - left = enum.IntEnum('Left', {'Shared': 1}) - right = enum.IntEnum('Right', {'Shared': 2, 'Only': 3}) - - class Pair(protocol): # type: ignore[misc,valid-type] - def make_pair(self, a: 'Any' = left.Shared, a_namespace: 'Any' = None, - b: 'Any' = right.Shared, b_namespace: 'Any' = None) -> 'tuple[int, int]': - return (self._make_index(a, namespace=a_namespace), - self._make_index(b, 0, namespace=b_namespace)) - - return Pair, left, right - - def test_shared_name_resolves_per_argument(self) -> None: - pair, _, _ = self._pair() - self.assertEqual(pair.make_pair(pair, a='Shared', b='Shared'), (1, 2)) - # and a name only one argument holds resolves normally - self.assertEqual(pair.make_pair(pair, a='Shared', b='Only'), (1, 3)) - - def test_ipv4_shared_name_falls_back_per_argument(self) -> None: - ipv4 = _attr('pcapkit.protocols.internet.ipv4.IPv4') - tos_del = _attr('pcapkit.const.ipv4.tos_del.ToSDelay') - - expected = ipv4(tos_del=tos_del.LOW, tos_thr=1).data - # ``LOW`` is a ToSDelay member but not a ToSThroughput one - self.assertEqual(ipv4(tos_del='LOW', tos_thr='LOW', tos_thr_default=1).data, expected) - self.assertEqual(ipv4(tos_thr='LOW', tos_thr_default=1).data, - ipv4(tos_thr=1).data) - - def test_non_parameter_first_argument_matches_every_argument(self) -> None: - enum = importlib.import_module('enum') - protocol = _attr('pcapkit.protocols.protocol.ProtocolBase') - level = enum.IntEnum('Level', {'HIGH': 7}) - - class Alias(protocol): # type: ignore[misc,valid-type] - def make_alias(self, b: 'Any' = level.HIGH, b_namespace: 'Any' = None) -> 'int': - value = b - return self._make_index(value) - - # ``value`` is a local, so ``b`` is found by its value - self.assertEqual(Alias.make_alias(Alias, b='HIGH'), 7) - - def test_unlocated_call_matches_every_argument(self) -> None: - # without the caller's source the call cannot be located, so every - # argument holding the name is a candidate and they must agree - module = importlib.import_module('pcapkit.protocols.protocol') - exc = _attr('pcapkit.utilities.exceptions.ProtocolNotImplemented') - pair, _, right = self._pair() - - with unittest.mock.patch.object(module, '_make_index_calls', lambda func: ()): - with self.assertRaisesRegex(exc, r"ambiguous member name 'Shared'.*" - r"pass a_namespace= or b_namespace= explicitly"): - pair.make_pair(pair, a='Shared', b='Shared') - self.assertEqual(pair.make_pair(pair, a='Shared', b='Shared', b_namespace=right), (1, 2)) - - def test_source_unavailable_locates_nothing(self) -> None: - module = importlib.import_module('pcapkit.protocols.protocol') - # a builtin has no Python source - self.assertEqual(module._make_index_calls(len), ()) - - def test_call_spans_to_its_last_line(self) -> None: - """A call split over lines ends on its last one, from Python 3.8 (#1709).""" - module = importlib.import_module('pcapkit.protocols.protocol') - - def make_split(self: 'Any', a: 'Any', a_namespace: 'Any' = None) -> 'Any': - return self._make_index(a, - namespace=a_namespace) - - first = make_split.__code__.co_firstlineno + 1 - self.assertEqual(module._make_index_calls(make_split), ((first, first + 1, 'a'),)) - - if __name__ == '__main__': unittest.main() diff --git a/tests/protocols/transport/test_dccp_unit.py b/tests/protocols/transport/test_dccp_unit.py index 5b332fc57e..868d178038 100644 --- a/tests/protocols/transport/test_dccp_unit.py +++ b/tests/protocols/transport/test_dccp_unit.py @@ -90,6 +90,7 @@ def setUp(self) -> None: from pcapkit.const.dccp.option import Option from pcapkit.const.dccp.packet_type import PacketType from pcapkit.const.dccp.reset_code import ResetCode + from pcapkit.const.reg.transtype import TransType from pcapkit.corekit.checksum.internet import ChecksumStatus from pcapkit.protocols.internet.ipv4 import IPv4 from pcapkit.protocols.internet.ipv6 import IPv6 @@ -100,6 +101,7 @@ def setUp(self) -> None: from pcapkit.utilities.warnings import ChecksumWarning, ProtocolWarning self.DCCP, self.IPv4, self.IPv6, self.IP_Pseudo = DCCP, IPv4, IPv6, IP_Pseudo + self.TransType = TransType self.Type, self.Option, self.Feature, self.ResetCode = PacketType, Option, Feature, ResetCode self.State, self.DropCode, self.Status = AckVectorState, DropCode, ChecksumStatus self.config, self.ProtocolError = config, ProtocolError @@ -502,15 +504,15 @@ def test_off_verifies_nothing(self) -> None: def test_the_ip_layer_computes_it(self) -> None: dccp = self.DCCP(srcport=SRC, dstport=DST, type=0, seq=1, service_code=0x12345678, options=[(self.Option.Change_L, {'feature': 1, 'value': b'\x02'})], payload=b'hello') - octets = self.IPv4(src='192.0.2.1', dst='198.51.100.2', protocol='DCCP', payload=dccp).data + octets = self.IPv4(src='192.0.2.1', dst='198.51.100.2', protocol=self.TransType.DCCP, payload=dccp).data self.assertEqual(octets, CHECKSUMMED[0]) dccp = self.DCCP(srcport=SRC, dstport=DST, type=7, seq=9, ack=8, reset_code=5, reset_data=b'\x20\x01\x02', payload=b'odd') - octets = self.IPv6(src='2001:db8::1', dst='2001:db8::2', next='DCCP', payload=dccp).data + octets = self.IPv6(src='2001:db8::1', dst='2001:db8::2', next=self.TransType.DCCP, payload=dccp).data self.assertEqual(octets, CHECKSUMMED[2]) def test_make_takes_the_pseudo_header(self) -> None: - pseudo = self.IP_Pseudo(src='192.0.2.1', dst='198.51.100.2', protocol='DCCP', upper_length=0) + pseudo = self.IP_Pseudo(src='192.0.2.1', dst='198.51.100.2', protocol=self.TransType.DCCP, upper_length=0) made = self.DCCP(srcport=SRC, dstport=DST, type=4, x=False, seq=7, ack=6, cscov=2, options=[(self.Option.Timestamp, {'timestamp': 5})], payload=b'0123456789abcdef', pseudo=pseudo).data @@ -518,14 +520,14 @@ def test_make_takes_the_pseudo_header(self) -> None: def test_a_coverage_reaching_the_end_is_written_as_zero(self) -> None: # As Linux's dccp_csum_outgoing: CsCov 0 wherever the coverage reaches the end. - pseudo = self.IP_Pseudo(src='192.0.2.1', dst='198.51.100.2', protocol='DCCP', upper_length=0) + pseudo = self.IP_Pseudo(src='192.0.2.1', dst='198.51.100.2', protocol=self.TransType.DCCP, upper_length=0) for cscov, payload in ((15, b'abc'), (2, b'abcd')): with self.subTest(cscov=cscov, payload=payload): made = self.DCCP(type=2, x=False, ccval=5, cscov=cscov, payload=payload, pseudo=pseudo).data self.assertEqual(made[5], 0x50) self.assertEqual(made[6:8], expected(pseudo4(len(made)), made, len(made))) dccp = self.DCCP(type=2, x=False, ccval=5, cscov=cscov, payload=payload) - octets = self.IPv4(src='192.0.2.1', dst='198.51.100.2', protocol='DCCP', payload=dccp).data + octets = self.IPv4(src='192.0.2.1', dst='198.51.100.2', protocol=self.TransType.DCCP, payload=dccp).data self.assertEqual(octets[20:], made) self.assertEqual(self.status(octets), (self.Status.VALID, [])) # One short of the end, and one written as given, keep their value. @@ -534,7 +536,7 @@ def test_a_coverage_reaching_the_end_is_written_as_zero(self) -> None: def test_off_or_given_is_written_as_is(self) -> None: with mock.patch.object(self.config, 'CHECKSUM_COMPUTE', False): - octets = self.IPv4(src='192.0.2.1', dst='198.51.100.2', protocol='DCCP', + octets = self.IPv4(src='192.0.2.1', dst='198.51.100.2', protocol=self.TransType.DCCP, payload=self.DCCP(type=2, x=False)).data self.assertEqual(octets[26:28], b'\x00\x00') self.assertEqual(self.DCCP(type=2, checksum=b'\xbe\xef').data[6:8], b'\xbe\xef') diff --git a/tests/protocols/transport/test_sctp_make_codes_2594_unit.py b/tests/protocols/transport/test_sctp_make_codes_2594_unit.py new file mode 100644 index 0000000000..d0fbc67eed --- /dev/null +++ b/tests/protocols/transport/test_sctp_make_codes_2594_unit.py @@ -0,0 +1,281 @@ +# -*- coding: utf-8 -*- +"""SCTP's enumeration fields take a member or an ``int`` code (GitHub issue #2594). + +The owner ruled, in review of #2593 (which closes #2489), that a ``make()`` field +holding an enumeration takes the member or an :class:`int`, resolved with the +enumeration's ``get``, and that nothing inside ``make()`` turns a name into a +member; and on #2617 that a code its field cannot carry, or a ``bool`` for a field +wider than one bit, raises ``ProtocolError``. A code the field carries but no +member holds is written as given, so that a capture of it rebuilds. SCTP's makers +took no ``X_default`` group, so this holds them to the rest of that rule. + +Each row of :data:`FIELDS` is one enumeration field of a chunk, parameter or +error cause maker, reached through ``SCTP.make(chunks=...)`` as a caller reaches +it, with its enumeration and the width :rfc:`9260` gives it. The chunk type, +parameter type and cause code that pick a maker are resolved the same way, as +the code of a ``(code, args)`` tuple, so each has a row too; a parsed list given +back to ``make`` has its keys checked as those codes are. + +Every case builds its own octets, so this belongs to the unit tier. + +""" +from __future__ import annotations + +import importlib +import re +import unittest +from typing import TYPE_CHECKING, NamedTuple +from unittest import mock + +from tests._support import reimport_once_per_class + +if TYPE_CHECKING: + from typing import Any, Callable, Optional + +#: Test tier (#1958): a per-protocol field table. +__tier__ = 'LATEST' + + +def _data_chunk(value: 'Any') -> 'list': + """A DATA chunk whose payload protocol identifier is ``value``.""" + from pcapkit.const.sctp.chunk import Chunk + + return [(Chunk.Payload_Data, {'ppid': value, 'data': b'\x01'})] + + +def _addrtypes_param(value: 'Any') -> 'list': + """An INIT chunk carrying a supported address types parameter listing ``value``.""" + from pcapkit.const.sctp.chunk import Chunk + from pcapkit.const.sctp.parameter import Parameter + + return [(Chunk.Initiation, {'parameters': [(Parameter.Supported_Address_Types, {'types': [value]})]})] + + +def _missing_param_cause(value: 'Any') -> 'list': + """An ERROR chunk carrying a missing mandatory parameter cause listing ``value``.""" + from pcapkit.const.sctp.cause_code import CauseCode + from pcapkit.const.sctp.chunk import Chunk + + return [(Chunk.Operation_Error, {'error': [(CauseCode.Missing_Mandatory_Parameter, {'types': [value]})]})] + + +class Row(NamedTuple): + """One enumeration field of an SCTP maker.""" + + #: The maker the field belongs to. + maker: 'str' + #: The field's keyword. + field: 'str' + #: The enumeration, as ``module:class``. + enum: 'str' + #: The field's width, in bits. + bits: 'int' + #: A member to build with. + member: 'str' + #: The field's name, as a refusal message ends it. + name: 'str' + #: The ``chunks`` argument of ``SCTP.make`` that carries a value for the field. + chunks: 'Callable[[Any], list]' + #: The field's value, read off the schema ``SCTP.make`` built. + built: 'Callable[[Any], Any]' + #: For a chunk, parameter or cause list, the ``chunks`` argument carrying the + #: parsed list of a packet's info re-keyed by a value: ``keyed(info, value)``. + keyed: 'Optional[Callable[[Any, Any], Any]]' = None + + +def _rekey(items: 'Any', value: 'Any') -> 'Any': + """The first item of the parsed list ``items``, as the one item of a list keyed by ``value``.""" + from pcapkit.corekit.multidict import OrderedMultiDict + + return OrderedMultiDict([(value, next(iter(items.items(multi=True)))[1])]) + + +def _chunk(value: 'Any') -> 'list': + """A SHUTDOWN COMPLETE chunk, typed ``value``.""" + return [(value, {})] + + +def _param(value: 'Any') -> 'list': + """An INIT chunk carrying a Forward-TSN-Supported parameter, typed ``value``.""" + from pcapkit.const.sctp.chunk import Chunk + + return [(Chunk.Initiation, {'parameters': [(value, {})]})] + + +def _cause(value: 'Any') -> 'list': + """An ERROR chunk carrying an out of resource cause, coded ``value``.""" + from pcapkit.const.sctp.chunk import Chunk + + return [(Chunk.Operation_Error, {'error': [(value, {})]})] + + +def _keyed_param(info: 'Any', value: 'Any') -> 'list': + """The INIT chunk of ``info``, its parameter list re-keyed by ``value``.""" + from pcapkit.const.sctp.chunk import Chunk + + init = next(iter(info.chunks.items(multi=True)))[1] + return [(Chunk.Initiation, {'parameters': _rekey(init.parameters, value)})] + + +def _keyed_cause(info: 'Any', value: 'Any') -> 'list': + """The ERROR chunk of ``info``, its cause list re-keyed by ``value``.""" + from pcapkit.const.sctp.chunk import Chunk + + error = next(iter(info.chunks.items(multi=True)))[1] + return [(Chunk.Operation_Error, {'error': _rekey(error.error, value)})] + + +#: Every enumeration field of SCTP's chunk, parameter and error cause makers. +#: Widths from :rfc:`9260`: Payload Protocol Identifier 32 bits (section 3.3.1), +#: Address Type 16 bits (section 3.3.2.1.5), Missing Param Type 16 bits +#: (section 3.3.10.2), Chunk Type 8 bits (section 3.2), Parameter Type 16 bits +#: (section 3.2.1) and Cause Code 16 bits (section 3.3.10), as the schema's +#: ``EnumField`` lengths agree. +FIELDS = ( + Row('_make_chunk_data', 'ppid', + 'pcapkit.const.sctp.payload_protocol_identifier:PayloadProtocolIdentifier', 32, + 'PayloadProtocolIdentifier_3GPP_NG_Application_Protocol', 'ppid', + _data_chunk, lambda schema: schema.chunks[0].ppid), + Row('_make_param_addrtypes', 'types', + 'pcapkit.const.sctp.parameter:Parameter', 16, 'IPv4_Address', 'parameter type', + _addrtypes_param, lambda schema: schema.chunks[0].parameters[0].types[0]), + Row('_make_cause_missing_param', 'types', + 'pcapkit.const.sctp.parameter:Parameter', 16, 'State_Cookie', 'parameter type', + _missing_param_cause, lambda schema: schema.chunks[0].error[0].types[0]), + Row('_make_sctp_chunks', 'type', 'pcapkit.const.sctp.chunk:Chunk', 8, 'Shutdown_Complete', 'chunk type', + _chunk, lambda schema: schema.chunks[0].type, lambda info, value: _rekey(info.chunks, value)), + Row('_make_sctp_parameters', 'type', 'pcapkit.const.sctp.parameter:Parameter', 16, 'Forward_TSN_supported', + 'parameter type', _param, lambda schema: schema.chunks[0].parameters[0].type, _keyed_param), + Row('_make_sctp_causes', 'code', 'pcapkit.const.sctp.cause_code:CauseCode', 16, 'Out_of_Resource', + 'cause code', _cause, lambda schema: schema.chunks[0].error[0].code, _keyed_cause), +) + + +class SCTPMakeCodeTests(unittest.TestCase): + """Each row refuses what its field cannot carry, and resolves a code with ``get``.""" + + def setUp(self) -> 'None': + reimport_once_per_class(self) + + @staticmethod + def _enum(row: 'Row') -> 'Any': + module, name = row.enum.split(':') + return getattr(importlib.import_module(module), name) + + @staticmethod + def _make(row: 'Row', value: 'Any', chunks: 'Any' = None) -> 'Any': + """The schema ``SCTP.make`` builds with ``value`` for the row's field, or from ``chunks``.""" + from pcapkit.protocols.transport.sctp import SCTP + + proto = SCTP.__new__(SCTP) + return SCTP.make(proto, srcport=9899, dstport=38412, chksum=b'\x00' * 4, + chunks=row.chunks(value) if chunks is None else chunks) + + def assertRefused(self, row: 'Row', value: 'Any', chunks: 'Any' = None) -> 'None': + """Building with ``value``, or from ``chunks``, raises ``ProtocolError`` naming the field.""" + from pcapkit.utilities.exceptions import ProtocolError + + try: + self._make(row, value, chunks) + except ProtocolError as error: + self.assertRegex(str(error), rf'^SCTP: invalid (.* )?{re.escape(row.name)}: {re.escape(repr(value))}$') + except Exception as error: # pylint: disable=broad-except + self.fail(f'{row.maker} {row.field}={value!r} raised {type(error).__name__}, not ProtocolError: {error}') + else: + self.fail(f'{row.maker} {row.field}={value!r} was accepted') + + def test_each_row_reaches_its_maker(self) -> 'None': + from pcapkit.protocols.transport.sctp import SCTP + + for row in FIELDS: + member = getattr(self._enum(row), row.member) + with self.subTest(maker=row.maker), \ + mock.patch.object(SCTP, row.maker, autospec=True, side_effect=getattr(SCTP, row.maker)) as maker: + self._make(row, member) + self.assertTrue(maker.called) + + def test_code_wider_than_the_field_is_refused(self) -> 'None': + for row in FIELDS: + with self.subTest(maker=row.maker, field=row.field): + self.assertRefused(row, 1 << row.bits) + + def test_negative_code_is_refused(self) -> 'None': + for row in FIELDS: + with self.subTest(maker=row.maker, field=row.field): + self.assertRefused(row, -1) + + def test_bool_is_refused(self) -> 'None': + for row in FIELDS: + self.assertGreater(row.bits, 1) + with self.subTest(maker=row.maker, field=row.field): + self.assertRefused(row, True) + + def test_name_is_refused(self) -> 'None': + for row in FIELDS: + with self.subTest(maker=row.maker, field=row.field): + self.assertRefused(row, row.member) + + def test_member_and_code_build_identical_octets(self) -> 'None': + for row in FIELDS: + member = getattr(self._enum(row), row.member) + with self.subTest(maker=row.maker, field=row.field): + self.assertEqual(self._make(row, member).pack(), self._make(row, int(member)).pack()) + + def test_code_resolves_to_its_member(self) -> 'None': + for row in FIELDS: + member = getattr(self._enum(row), row.member) + with self.subTest(maker=row.maker, field=row.field): + self.assertIs(row.built(self._make(row, int(member))), member) + + def test_a_parsed_list_has_its_keys_checked(self) -> 'None': + """A parsed list rebuilds as it was, and one keyed by a refused code is refused.""" + from pcapkit.protocols.transport.sctp import SCTP + + for row in FIELDS: + if row.keyed is None: + continue + member = getattr(self._enum(row), row.member) + octets = self._make(row, member).pack() + info = SCTP(octets, len(octets)).info + with self.subTest(maker=row.maker): + self.assertEqual(self._make(row, member, row.keyed(info, member)).pack(), octets) + for value in (1 << row.bits, -1, True, row.member): + self.assertRefused(row, value, row.keyed(info, value)) + + def test_code_no_member_holds_is_written_as_given(self) -> 'None': + """A code the field carries but no member holds builds, and its capture + rebuilds byte for byte.""" + from pcapkit.protocols.transport.sctp import SCTP + + memberless = {} # type: dict[str, list[int]] + for row in FIELDS: + with self.subTest(maker=row.maker, field=row.field): + if row.bits > 16: + self.skipTest('too wide to search for a code no member holds') + if row.enum not in memberless: + enum = self._enum(row) + codes = [] + for code in range(1 << row.bits): + try: + enum.get(code) + except ValueError: + codes.append(code) + memberless[row.enum] = codes + if not memberless[row.enum]: + self.skipTest(f'a member holds every {row.bits}-bit code of {row.enum}') + for code in memberless[row.enum]: + try: + schema = self._make(row, code) + except Exception as error: # pylint: disable=broad-except + self.fail(f'{row.maker} {row.field}={code!r} raised {type(error).__name__}: {error}') + self.assertEqual(int(row.built(schema)), code) + octets = schema.pack() + parsed = SCTP(octets, len(octets)) + proto = SCTP.__new__(SCTP) + rebuilt = SCTP.make(proto, srcport=9899, dstport=38412, chksum=b'\x00' * 4, + chunks=parsed.info.chunks) + self.assertEqual(rebuilt.pack(), octets) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/protocols/transport/test_tcp_checksum_2473_unit.py b/tests/protocols/transport/test_tcp_checksum_2473_unit.py index 52c3b7f8a3..30f377209d 100644 --- a/tests/protocols/transport/test_tcp_checksum_2473_unit.py +++ b/tests/protocols/transport/test_tcp_checksum_2473_unit.py @@ -60,6 +60,7 @@ class TCPChecksumTests(unittest.TestCase): def setUp(self) -> None: reimport_once_per_class(self) + from pcapkit.const.reg.transtype import TransType from pcapkit.corekit.checksum.internet import ChecksumStatus from pcapkit.protocols.internet.ipv4 import IPv4 from pcapkit.protocols.internet.ipv6 import IPv6 @@ -69,6 +70,7 @@ def setUp(self) -> None: from pcapkit.utilities.warnings import ChecksumWarning, ProtocolWarning self.IPv4, self.IPv6, self.TCP, self.IP_Pseudo = IPv4, IPv6, TCP, IP_Pseudo + self.TransType = TransType self.Status = ChecksumStatus self.config = config self.ProtocolWarning = ProtocolWarning @@ -145,10 +147,10 @@ def test_an_upper_layer_the_pseudo_header_does_not_name(self) -> None: def test_ipv6_and_truncation(self) -> None: segment = self.TCP(srcport=80, dstport=40000, seq_no=1, ack=True, ack_no=2, payload=b'odd').data with mock.patch.object(self.config, 'CHECKSUM_COMPUTE', True): - packet = self.IPv6(src='2001:db8::1', dst='2001:db8::2', next='TCP', payload=segment).data + packet = self.IPv6(src='2001:db8::1', dst='2001:db8::2', next=self.TransType.TCP, payload=segment).data self.assertEqual(self.status(packet, cls=self.IPv6), (self.Status.NOT_COMPUTED, [])) - made = self.IPv6(src='2001:db8::1', dst='2001:db8::2', next='TCP', + made = self.IPv6(src='2001:db8::1', dst='2001:db8::2', next=self.TransType.TCP, payload=self.TCP(srcport=80, dstport=40000, payload=b'odd')).data self.assertEqual(made[56:58], expected(pseudo6(wire.IPV6_A, wire.IPV6_B, 23), made[40:])) self.assertEqual(self.status(made, cls=self.IPv6), (self.Status.VALID, [])) @@ -165,7 +167,7 @@ def make(self, compute: bool = True, **kwargs: object) -> bytes: """The octets of an IPv4 packet carrying a TCP segment made with ``kwargs``.""" with mock.patch.object(self.config, 'CHECKSUM_COMPUTE', compute): tcp = self.TCP(srcport=40000, dstport=80, seq_no=7, syn=True, payload=b'hello', **kwargs) - return self.IPv4(src='192.0.2.1', dst='198.51.100.2', ttl=64, protocol='TCP', payload=tcp).data + return self.IPv4(src='192.0.2.1', dst='198.51.100.2', ttl=64, protocol=self.TransType.TCP, payload=tcp).data def test_the_ip_layer_computes_it(self) -> None: octets = self.make() @@ -179,7 +181,7 @@ def test_a_source_route_names_the_destination(self) -> None: from pcapkit.const.ipv4.option_number import OptionNumber tcp = self.TCP(srcport=40000, dstport=80, payload=b'routed') - octets = self.IPv4(src='192.0.2.1', dst='198.51.100.2', protocol='TCP', payload=tcp, + octets = self.IPv4(src='192.0.2.1', dst='198.51.100.2', protocol=self.TransType.TCP, payload=tcp, options=[(OptionNumber.LSR, {'counts': 2, 'route': ['203.0.113.1'], 'remaining': ['203.0.113.9']})]).data hdr = (octets[0] & 0x0F) * 4 @@ -191,7 +193,7 @@ def test_a_wrong_header_checksum_warns_once(self) -> None: with mock.patch.object(self.config, 'CHECKSUM_VERIFY', True), \ warnings.catch_warnings(record=True) as caught: warnings.simplefilter('always') - ipv4 = self.IPv4(src='192.0.2.1', dst='198.51.100.2', protocol='TCP', checksum=b'\xbe\xef', + ipv4 = self.IPv4(src='192.0.2.1', dst='198.51.100.2', protocol=self.TransType.TCP, checksum=b'\xbe\xef', payload=self.TCP(srcport=40000, dstport=80)) messages = [(item.category, str(item.message)) for item in caught if issubclass(item.category, self.ProtocolWarning)] @@ -201,7 +203,7 @@ def test_a_wrong_header_checksum_warns_once(self) -> None: self.assertIs(ipv4['TCP'].info.checksum_status, self.Status.VALID) def test_make_takes_the_pseudo_header(self) -> None: - pseudo = self.IP_Pseudo(src='192.0.2.1', dst='198.51.100.2', protocol='TCP', upper_length=0) + pseudo = self.IP_Pseudo(src='192.0.2.1', dst='198.51.100.2', protocol=self.TransType.TCP, upper_length=0) segment = self.TCP(srcport=40000, dstport=80, payload=b'alone', pseudo=pseudo).data prefix = wire.IPV4_A + wire.IPV4_B + b'\x00\x06' + len(segment).to_bytes(2, 'big') self.assertEqual(segment[16:18], expected(prefix, segment)) diff --git a/tests/protocols/transport/test_transport_make_codes_2594_unit.py b/tests/protocols/transport/test_transport_make_codes_2594_unit.py new file mode 100644 index 0000000000..e0d0bc1116 --- /dev/null +++ b/tests/protocols/transport/test_transport_make_codes_2594_unit.py @@ -0,0 +1,217 @@ +# -*- coding: utf-8 -*- +"""The TCP and DCCP makers take an enumeration field as a member or a code (#2594). + +Ruled in review of #2593 (#2489), and carried across the makers by #2594: a +maker's enumeration field takes the member or an :class:`int` code, resolved +with the enumeration's ``get``, and refuses a name rather than resolving it +against an enumeration inferred from the caller's frame. A caller holding a +name resolves it first, with :meth:`Protocol.resolve_index +`. Under #2617 a code the +field cannot carry, and a :obj:`bool` for a field wider than one bit, raise +:exc:`~pcapkit.utilities.exceptions.ProtocolError` too. + +Each row of :data:`FIELDS` is one such field, reached the way a caller reaches +it: an option maker through ``options=``, a header field as a keyword of +``make()``, and the kind or type of each option in ``options=`` as the code of +its ``(code, args)`` tuple. The width is the field's own, from the schema and +the RFC. A parsed option list given back to ``make()`` has its keys checked as +those codes are. + +Every case builds its own octets, so this belongs to the unit tier. + +""" +from __future__ import annotations + +import importlib +import re +import unittest +from typing import TYPE_CHECKING, NamedTuple + +from tests._support import reimport_once_per_class + +if TYPE_CHECKING: + from typing import Any + + +class Field(NamedTuple): + """An enumeration field of a maker.""" + + #: The maker, as ``Class.method``; the class is the alias of its refusals. + maker: 'str' + #: The keyword the field is given by, in ``make()`` or the option's arguments. + keyword: 'str' + #: The enumeration, as ``module:Class`` under :mod:`pcapkit.const`. + enum: 'str' + #: A member of :attr:`enum` that the field carries. + member: 'str' + #: The width of the field, in bits. + bits: 'int' + #: The name of the field in the maker's refusal. + name: 'str' + + +#: The fields, one row per maker and field. The widths: the TCP Alternate +#: Checksum Request ``Chksum`` octet (RFC 1146) and the 4-bit Multipath TCP +#: subtype (RFC 8684 section 3); the 4-bit DCCP Type and the one-octet Reset +#: Code (RFC 4340 sections 5.1 and 5.6), the one-octet Feature Number of +#: Change and Confirm (section 6), the 2-bit Ack Vector state (section 11.4) +#: and the 3-bit Data Dropped drop code (section 11.7); the one-octet TCP +#: option kind (RFC 9293 section 3.1) and DCCP option type (RFC 4340 section 5.8). +FIELDS = ( + Field('TCP._make_mode_chkreq', 'algorithm', 'tcp.checksum:Checksum', + 'Checksum_8_bit_Fletcher_s_algorithm', 8, '[OptNo 14] algorithm'), + Field('TCP._make_mode_mp', 'subtype', 'tcp.mp_tcp_option:MPTCPOption', 'MP_PRIO', 4, '[OptNo 30] subtype'), + Field('DCCP.make', 'type', 'dccp.packet_type:PacketType', 'DCCP_Ack', 4, 'type'), + Field('DCCP.make', 'reset_code', 'dccp.reset_code:ResetCode', 'Option_Error', 8, 'reset code'), + Field('DCCP._make_opt_change', 'feature', 'dccp.feature:Feature', 'Send_Ack_Vector', 8, '[Opt 32] feature'), + Field('DCCP._make_opt_confirm', 'feature', 'dccp.feature:Feature', 'Send_Ack_Vector', 8, '[Opt 35] feature'), + Field('DCCP._make_opt_ack_vector', 'state', 'dccp.ack_vector_state:AckVectorState', + 'Not_Yet_Received', 2, '[Opt 38] Ack Vector state'), + Field('DCCP._make_opt_data_dropped', 'code', 'dccp.drop_code:DropCode', 'Corrupt', 3, '[Opt 40] drop code'), + Field('TCP._make_tcp_options', 'kind', 'tcp.option:Option', 'No_Operation', 8, 'option kind'), + Field('DCCP._make_dccp_options', 'type', 'dccp.option:Option', 'Slow_Receiver', 8, 'option type'), +) + +#: The makers of :data:`FIELDS` that take an option list, as a parsed one is +#: given back to ``make()``. +OPTION_LISTS = ('TCP._make_tcp_options', 'DCCP._make_dccp_options') + + +class EnumerationFieldTests(unittest.TestCase): + """Each field in :data:`FIELDS` takes a member or a code, and refuses the rest.""" + + def setUp(self) -> 'None': + reimport_once_per_class(self) + + @staticmethod + def enumeration(field: 'Field') -> 'Any': + """The enumeration ``field`` holds.""" + module, name = field.enum.split(':') + return getattr(importlib.import_module(f'pcapkit.const.{module}'), name) + + @staticmethod + def build(field: 'Field', value: 'Any') -> 'bytes': + """The octets of a packet carrying ``value`` in ``field``.""" + from pcapkit.const.dccp.option import Option as DCCP_Option + from pcapkit.const.dccp.packet_type import PacketType + from pcapkit.const.tcp.option import Option as TCP_Option + from pcapkit.protocols.transport.dccp import DCCP + from pcapkit.protocols.transport.tcp import TCP + + def dccp_option(code: 'Any', args: 'dict[str, Any]') -> 'bytes': + return DCCP(type=PacketType.DCCP_Ack, ack=1, checksum=b'\x00\x00', options=[(code, args)]).data + + builders = { + 'TCP._make_mode_chkreq': lambda: TCP( + options=[(TCP_Option.TCP_Alternate_Checksum_Request, {'algorithm': value})]).data, + # MP_PRIO takes an Address ID, and RFC 8684 section 3.3.8 an ACK. + 'TCP._make_mode_mp': lambda: TCP( + ack=True, options=[(TCP_Option.Multipath_TCP, {'subtype': value, 'addr_id': 1})]).data, + 'DCCP.make': lambda: DCCP(**{'type': PacketType.DCCP_Reset, 'ack': 1, 'checksum': b'\x00\x00', + field.keyword: value}).data, + 'DCCP._make_opt_change': lambda: dccp_option(DCCP_Option.Change_L, {'feature': value, 'value': b'\x01'}), + 'DCCP._make_opt_confirm': lambda: dccp_option(DCCP_Option.Confirm_R, {'feature': value, 'value': b'\x01'}), + 'DCCP._make_opt_ack_vector': lambda: dccp_option(DCCP_Option.Ack_Vector_Nonce_0, {'vector': [(value, 3)]}), + 'DCCP._make_opt_data_dropped': lambda: dccp_option(DCCP_Option.Data_Dropped, {'vector': [(value, 2)]}), + 'TCP._make_tcp_options': lambda: TCP(options=[(value, {})]).data, + 'DCCP._make_dccp_options': lambda: dccp_option(value, {}), + } + return builders[field.maker]() + + @staticmethod + def build_list(field: 'Field', options: 'Any') -> 'bytes': + """The octets of a packet carrying the option list ``options``.""" + from pcapkit.const.dccp.packet_type import PacketType + from pcapkit.protocols.transport.dccp import DCCP + from pcapkit.protocols.transport.tcp import TCP + + if field.maker == 'TCP._make_tcp_options': + return TCP(options=options).data + return DCCP(type=PacketType.DCCP_Ack, ack=1, checksum=b'\x00\x00', options=options).data + + def assertRefused(self, field: 'Field', value: 'Any', options: 'Any' = None) -> 'None': # pylint: disable=invalid-name + """Building ``value`` in ``field``, or the option list ``options``, raises the ``ProtocolError`` naming it.""" + from pcapkit.utilities.exceptions import ProtocolError + + alias = field.maker.split('.')[0] + try: + if options is None: + self.build(field, value) + else: + self.build_list(field, options) + except ProtocolError as error: + self.assertRegex(str(error), rf'^{alias}: invalid {re.escape(field.name)}: {re.escape(repr(value))}$') + except Exception as error: # pylint: disable=broad-except + self.fail(f'{field.maker} {field.keyword}={value!r} raised {type(error).__name__}, ' + f'not ProtocolError: {error}') + else: + self.fail(f'{field.maker} {field.keyword}={value!r} was accepted') + + def test_a_code_past_the_field_is_refused(self) -> 'None': + for field in FIELDS: + with self.subTest(maker=field.maker, field=field.keyword, value=1 << field.bits): + self.assertRefused(field, 1 << field.bits) + + def test_a_negative_code_is_refused(self) -> 'None': + for field in FIELDS: + with self.subTest(maker=field.maker, field=field.keyword): + self.assertRefused(field, -1) + + def test_a_bool_is_refused_for_a_field_wider_than_one_bit(self) -> 'None': + for field in FIELDS: + if field.bits > 1: + with self.subTest(maker=field.maker, field=field.keyword): + self.assertRefused(field, True) + + def test_a_name_is_refused_and_resolve_index_resolves_it(self) -> 'None': + from pcapkit.protocols.protocol import Protocol + + for field in FIELDS: + namespace = self.enumeration(field) + with self.subTest(maker=field.maker, field=field.keyword, value=field.member): + self.assertRefused(field, field.member) + self.assertEqual(self.build(field, Protocol.resolve_index(field.member, namespace=namespace)), + self.build(field, namespace[field.member])) + + def test_a_member_and_its_code_build_the_same_octets(self) -> 'None': + for field in FIELDS: + member = self.enumeration(field)[field.member] + with self.subTest(maker=field.maker, field=field.keyword, member=member): + self.assertEqual(self.build(field, member), self.build(field, int(member))) + + def test_every_code_the_field_carries_is_taken(self) -> 'None': + """The width is not too narrow: the widest code builds, as does zero. + + Where no member holds the code, as for TCP option kind 255, the code is + written as given. + + """ + for field in FIELDS: + for code in (0, (1 << field.bits) - 1): + try: + resolved = self.enumeration(field).get(code) + except ValueError: + resolved = code + with self.subTest(maker=field.maker, field=field.keyword, code=code): + self.assertEqual(self.build(field, code), self.build(field, resolved)) + + def test_a_parsed_option_list_has_its_keys_checked(self) -> 'None': + """A parsed option list rebuilds as it was, and one keyed by a refused code is refused.""" + from pcapkit.corekit.multidict import OrderedMultiDict + from pcapkit.protocols.transport.dccp import DCCP + from pcapkit.protocols.transport.tcp import TCP + + for field in FIELDS: + if field.maker not in OPTION_LISTS: + continue + octets = self.build(field, self.enumeration(field)[field.member]) + parsed = (TCP if field.maker.startswith('TCP') else DCCP)(octets, len(octets)).info.options + option = next(iter(parsed.items(multi=True)))[1] + with self.subTest(maker=field.maker): + self.assertEqual(self.build_list(field, parsed), octets) + for value in (1 << field.bits, -1, True, field.member): + self.assertRefused(field, value, OrderedMultiDict([(value, option)])) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/protocols/transport/test_udp_checksum_2474_unit.py b/tests/protocols/transport/test_udp_checksum_2474_unit.py index 1f545c8e59..42804a906e 100644 --- a/tests/protocols/transport/test_udp_checksum_2474_unit.py +++ b/tests/protocols/transport/test_udp_checksum_2474_unit.py @@ -62,6 +62,7 @@ class UDPChecksumTests(unittest.TestCase): def setUp(self) -> None: reimport_once_per_class(self) + from pcapkit.const.reg.transtype import TransType from pcapkit.corekit.checksum.internet import ChecksumStatus from pcapkit.protocols.internet.ipv4 import IPv4 from pcapkit.protocols.internet.ipv6 import IPv6 @@ -71,6 +72,7 @@ def setUp(self) -> None: from pcapkit.utilities.warnings import ChecksumWarning, ProtocolWarning self.IPv4, self.IPv6, self.UDP, self.IP_Pseudo = IPv4, IPv6, UDP, IP_Pseudo + self.TransType = TransType self.Status = ChecksumStatus self.config = config self.ProtocolWarning = ProtocolWarning @@ -94,7 +96,7 @@ def status(self, octets: bytes, **kwargs: object) -> 'tuple[object, list[tuple[t def make6(self, **kwargs: object) -> bytes: """The octets of an IPv6 packet carrying a UDP datagram made with ``kwargs``.""" udp = self.UDP(srcport=40000, dstport=9, payload=b'hello', **kwargs) - return self.IPv6(src='2001:db8::1', dst='2001:db8::2', next='UDP', payload=udp).data + return self.IPv6(src='2001:db8::1', dst='2001:db8::2', next=self.TransType.UDP, payload=udp).data def test_the_captured_checksum_verifies(self) -> None: self.assertEqual(expected(pseudo4(CAPTURED[12:16], CAPTURED[16:20], 125), CAPTURED[20:]), CAPTURED[26:28]) @@ -135,7 +137,7 @@ def test_parse_rebuild_is_exact(self) -> None: self.assertEqual(self.IPv4.from_data(layer.info).data, packet) def test_the_ip_layer_computes_it(self) -> None: - octets = self.IPv4(src='192.0.2.1', dst='198.51.100.2', protocol='UDP', + octets = self.IPv4(src='192.0.2.1', dst='198.51.100.2', protocol=self.TransType.UDP, payload=self.UDP(srcport=40000, dstport=9, payload=b'hello')).data self.assertEqual(octets[26:28], expected(pseudo4(wire.IPV4_A, wire.IPV4_B, 13), octets[20:])) self.assertEqual(self.status(octets), (self.Status.VALID, [])) @@ -151,14 +153,14 @@ def test_a_fragment_leaves_it_out(self) -> None: for kwargs in ({'mf': True}, {'offset': 185}): with self.subTest(**kwargs): udp = self.UDP(srcport=40000, dstport=9, payload=b'hello') - octets = self.IPv4(src='192.0.2.1', dst='198.51.100.2', protocol='UDP', payload=udp, **kwargs).data + octets = self.IPv4(src='192.0.2.1', dst='198.51.100.2', protocol=self.TransType.UDP, payload=udp, **kwargs).data self.assertEqual(octets[20:], udp.data) self.assertEqual(octets[26:28], b'\x00\x00') def test_the_length_field_bounds_it(self) -> None: # The trailer past the Length field is neither summed nor in the pseudo-header. udp = self.UDP(srcport=40000, dstport=9, payload=b'hello', trailer=b'pad') - octets = self.IPv4(src='192.0.2.1', dst='198.51.100.2', protocol='UDP', payload=udp).data + octets = self.IPv4(src='192.0.2.1', dst='198.51.100.2', protocol=self.TransType.UDP, payload=udp).data self.assertEqual(octets[26:28], expected(pseudo4(wire.IPV4_A, wire.IPV4_B, 13), octets[20:33])) self.assertEqual(self.status(octets), (self.Status.VALID, [])) @@ -167,19 +169,19 @@ def test_a_computed_zero_is_sent_as_all_ones(self) -> None: prefix = pseudo4(wire.IPV4_A, wire.IPV4_B, 10) rest = ~wire.checksum(prefix + b'\x9c\x40\x00\x09\x00\x0a\x00\x00') & 0xFFFF word = (0xFFFF - rest).to_bytes(2, 'big') - octets = self.IPv4(src='192.0.2.1', dst='198.51.100.2', protocol='UDP', + octets = self.IPv4(src='192.0.2.1', dst='198.51.100.2', protocol=self.TransType.UDP, payload=self.UDP(srcport=40000, dstport=9, payload=word)).data self.assertEqual(wire.checksum(prefix + octets[20:26] + b'\x00\x00' + octets[28:]), 0) self.assertEqual(octets[26:28], b'\xff\xff') self.assertEqual(self.status(octets), (self.Status.VALID, [])) def test_make_takes_the_pseudo_header(self) -> None: - pseudo = self.IP_Pseudo(src='192.0.2.1', dst='198.51.100.2', protocol='UDP', upper_length=0) + pseudo = self.IP_Pseudo(src='192.0.2.1', dst='198.51.100.2', protocol=self.TransType.UDP, upper_length=0) datagram = self.UDP(srcport=40000, dstport=9, payload=b'alone', pseudo=pseudo).data self.assertEqual(datagram[6:8], expected(pseudo4(wire.IPV4_A, wire.IPV4_B, 13), datagram)) # A jumbogram's zero Length (RFC 2675): the datagram is all but the trailer. - pseudo = self.IP_Pseudo(src='2001:db8::1', dst='2001:db8::2', protocol='UDP', upper_length=0) + pseudo = self.IP_Pseudo(src='2001:db8::1', dst='2001:db8::2', protocol=self.TransType.UDP, upper_length=0) datagram = self.UDP(srcport=40000, dstport=9, payload=b'alone', total_length=0, trailer=b'pad', pseudo=pseudo).data self.assertEqual(datagram[6:8], expected(pseudo6(wire.IPV6_A, wire.IPV6_B, 13), datagram[:13])) @@ -189,7 +191,7 @@ def test_make_agrees_with_scapy(self) -> None: from scapy.layers.inet import IP from scapy.layers.inet import UDP as ScapyUDP - octets = self.IPv4(src='192.0.2.1', dst='198.51.100.2', protocol='UDP', + octets = self.IPv4(src='192.0.2.1', dst='198.51.100.2', protocol=self.TransType.UDP, payload=self.UDP(srcport=40000, dstport=9, payload=b'hello')).data packet = IP(octets) del packet[ScapyUDP].chksum