From 92fd4ca9f79698771c1645ef1d0a9717f91a479c Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Sun, 1 Nov 2026 02:36:44 -0500 Subject: [PATCH 1/2] refactor(internet): name IGMP member classes after their modules (#2529) - Rename igmpv1/igmpv2/igmpv3.IGMP to IGMPv1/IGMPv2/IGMPv3, and the igmpv3 schema and data classes to IGMPv3; id(), alias, registry keys and package exports are unchanged, and info_name now follows the class name. - Update the dispatch imports, docstrings, API pages, ext.rst rows and the Mermaid click anchors to the new names. - Merge igmp.py's two versionadded directives into one bare tag and drop igmpv3.py's restating body (#2530). - Record the rulings: a protocol class takes its module's name and a module holds one protocol class (protocol-family-grouping), and the versionadded rule (documentation). A new test pins the class names, __all__ and info_name. --- .../conventions/documentation.rst | 10 + .../source/contributing/conventions/index.rst | 3 +- .../conventions/protocol-family-grouping.rst | 25 +- docs/source/contributing/pep.rst | 6 +- docs/source/ext.rst | 6 +- docs/source/pcapkit/protocols/index.rst | 6 +- .../pcapkit/protocols/internet/igmp.rst | 6 +- .../pcapkit/protocols/internet/igmpv1.rst | 6 +- .../pcapkit/protocols/internet/igmpv2.rst | 6 +- .../pcapkit/protocols/internet/igmpv3.rst | 12 +- examples/generators/dispatch.py | 4 +- pcapkit/protocols/data/internet/__init__.py | 2 +- pcapkit/protocols/data/internet/igmpv3.py | 14 +- pcapkit/protocols/internet/__init__.py | 6 +- pcapkit/protocols/internet/igmp.py | 24 +- pcapkit/protocols/internet/igmpv1.py | 8 +- pcapkit/protocols/internet/igmpv2.py | 8 +- pcapkit/protocols/internet/igmpv3.py | 34 ++- pcapkit/protocols/internet/internet.py | 6 +- pcapkit/protocols/schema/internet/__init__.py | 2 +- pcapkit/protocols/schema/internet/igmpv3.py | 28 +-- tests/corekit/test_schema_roundtrip_unit.py | 2 +- tests/protocols/internet/test_igmp_unit.py | 232 ++++++++++-------- 23 files changed, 244 insertions(+), 212 deletions(-) diff --git a/docs/source/contributing/conventions/documentation.rst b/docs/source/contributing/conventions/documentation.rst index b73f87df85..5c62de565c 100644 --- a/docs/source/contributing/conventions/documentation.rst +++ b/docs/source/contributing/conventions/documentation.rst @@ -303,6 +303,16 @@ deprecation note where a user could have depended on the thing, and deleted outr where it never shipped. The ``rarp`` entry above needed no note for that second reason. Asked and ruled on :issue:`719`. +Version Directives +~~~~~~~~~~~~~~~~~~ + +**One** ``versionadded`` **per version in a docstring or prose block.** Directives of +the same version there merge into one. **A body that only names the thing added and +cites its issue or pull request is dropped**, leaving the bare tag: the summary above +already names the thing, so the body adds nothing. A body with content of its own +stays, such as a move (*Moved here from* ...), a rename or a takeover. Ruled on +:issue:`2530`. + Format and Mechanics ~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/source/contributing/conventions/index.rst b/docs/source/contributing/conventions/index.rst index 59e492102b..c635dc7922 100644 --- a/docs/source/contributing/conventions/index.rst +++ b/docs/source/contributing/conventions/index.rst @@ -16,7 +16,8 @@ House Conventions * :ref:`registry-protocol` -- :mod:`pcapkit.const` again; * :ref:`extension-header-subclassing` -- a protocol class hierarchy; * :ref:`protocol-family-grouping` -- how a protocol family's base and members - spell :meth:`~pcapkit.protocols.protocol.Protocol.id`; + spell :meth:`~pcapkit.protocols.protocol.Protocol.id`, and how a protocol class + is named after its module; * :ref:`protocol-member-order` -- the order of members in a protocol class body; * :ref:`protocol-layer-placement` -- which subpackage a dissector belongs in; * :ref:`process` -- the repository rather than any of its code; diff --git a/docs/source/contributing/conventions/protocol-family-grouping.rst b/docs/source/contributing/conventions/protocol-family-grouping.rst index 76bdbc98c3..21e85f55f9 100644 --- a/docs/source/contributing/conventions/protocol-family-grouping.rst +++ b/docs/source/contributing/conventions/protocol-family-grouping.rst @@ -66,9 +66,9 @@ Member Shapes - the member's own name only - :class:`~pcapkit.protocols.internet.ah.AH` ``('AH',)``, :class:`~pcapkit.protocols.internet.esp.ESP` ``('ESP',)``; - :class:`IGMPv2 ` ``('IGMPv2',)`` and - :class:`IGMPv3 ` ``('IGMPv3',)`` -- - :class:`IGMPv1 ` narrows the same way, + :class:`IGMPv2 ` ``('IGMPv2',)`` and + :class:`IGMPv3 ` ``('IGMPv3',)`` -- + :class:`IGMPv1 ` narrows the same way, but to the RFC canonical name rather than a versioned one (:ref:`protocol-family-canonical-names`) * - Family-inclusive @@ -121,10 +121,10 @@ and its ``name`` carries no "version 4". ``ICMPv6`` keeps its own version, since **IGMPv1 follows the same shape, by the same reasoning** (:issue:`2439`, :pr:`2506`). :rfc:`1112` names IGMPv1's protocol plain ``IGMP``, so -:meth:`IGMPv1.id() ` is ``('IGMP',)`` and +:meth:`IGMPv1.id() ` is ``('IGMP',)`` and its ``alias`` is ``'IGMP'``, while -:meth:`IGMPv2.id() ` and -:meth:`IGMPv3.id() ` keep their own +:meth:`IGMPv2.id() ` and +:meth:`IGMPv3.id() ` keep their own versions, ``('IGMPv2',)`` and ``('IGMPv3',)``, since :rfc:`2236` and :rfc:`3376` name them with one. The family base's ``id()`` follows: :meth:`IGMP.id() ` is @@ -175,5 +175,14 @@ No Invented Suffixes :class:`~pcapkit.protocols.application.http.HTTP`, :class:`~pcapkit.protocols.link.vlan.VLAN` and :class:`~pcapkit.protocols.link.l2tp.L2TP` are the precedent. No ``_Family`` suffix is -added (:pr:`2446`). A member may take a versioned class name, as -:ref:`above ` describes. +added (:pr:`2446`). + +**A protocol class takes its module's name**, whatever its canonical name and +``id()``. The family base takes the family's name and a member its module's versioned +name: :mod:`~pcapkit.protocols.internet.igmpv1` holds +:class:`~pcapkit.protocols.internet.igmpv1.IGMPv1`, though its ``id()`` is ``('IGMP',)`` +(:ref:`above `). **A module holds one protocol +class.** :mod:`~pcapkit.protocols.protocol` is exempt: its +:class:`~pcapkit.protocols.protocol.ProtocolBase` and +:class:`~pcapkit.protocols.protocol.Protocol` are framework bases rather than +dissectors. Both ruled on :issue:`2529`. diff --git a/docs/source/contributing/pep.rst b/docs/source/contributing/pep.rst index d429a6f885..e275feb088 100644 --- a/docs/source/contributing/pep.rst +++ b/docs/source/contributing/pep.rst @@ -1101,9 +1101,9 @@ checksums (ICMPv6 does; ICMPv4 does not) and parse without checksum computation (:issue:`2470`). **IGMP — done within wave 2** (:issue:`2439`, :issue:`2448`). -The three concrete classes :class:`IGMPv1 `, -:class:`IGMPv2 ` and -:class:`IGMPv3 ` share one read path on +The three concrete classes :class:`IGMPv1 `, +:class:`IGMPv2 ` and +:class:`IGMPv3 ` share one read path on the abstract :class:`~pcapkit.protocols.internet.igmp.IGMP` base, which :meth:`~pcapkit.protocols.internet.igmp.IGMP._classify` dispatches to the right one. IGMPv1/v2 share one 8-octet message shape; IGMPv3 (:rfc:`3376`) adds the diff --git a/docs/source/ext.rst b/docs/source/ext.rst index cde3dd6e60..e691a75652 100644 --- a/docs/source/ext.rst +++ b/docs/source/ext.rst @@ -80,11 +80,11 @@ The protocol classes :mod:`pcapkit` ships: + +----------------+-----------------------+-------------------------------------------------------------+ | | | :class:`pcapkit.protocols.internet.igmp.IGMP` | + + +-----------------------+-------------------------------------------------------------+ -| | | :class:`pcapkit.protocols.internet.igmpv1.IGMP` | +| | | :class:`pcapkit.protocols.internet.igmpv1.IGMPv1` | + + IGMP Family +-----------------------+-------------------------------------------------------------+ -| | | :class:`pcapkit.protocols.internet.igmpv2.IGMP` | +| | | :class:`pcapkit.protocols.internet.igmpv2.IGMPv2` | + + +-----------------------+-------------------------------------------------------------+ -| | | :class:`pcapkit.protocols.internet.igmpv3.IGMP` | +| | | :class:`pcapkit.protocols.internet.igmpv3.IGMPv3` | +------------------------------------------------------------------+----------------+-----------------------+-------------------------------------------------------------+ | Transport Layer | :class:`pcapkit.protocols.transport.tcp.TCP` | + (:class:`~pcapkit.protocols.transport.transport.Transport` +----------------+-----------------------+-------------------------------------------------------------+ diff --git a/docs/source/pcapkit/protocols/index.rst b/docs/source/pcapkit/protocols/index.rst index 3bbe5f0fe7..83847e49b0 100644 --- a/docs/source/pcapkit/protocols/index.rst +++ b/docs/source/pcapkit/protocols/index.rst @@ -148,9 +148,9 @@ from given information. The class hierarchy of :mod:`pcapkit.protocols`: click ESP "internet/esp.html#pcapkit.protocols.internet.esp.ESP" click HIP "internet/hip.html#pcapkit.protocols.internet.hip.HIP" click IGMP "internet/igmp.html#pcapkit.protocols.internet.igmp.IGMP" - click IGMPv1 "internet/igmpv1.html#pcapkit.protocols.internet.igmpv1.IGMP" - click IGMPv2 "internet/igmpv2.html#pcapkit.protocols.internet.igmpv2.IGMP" - click IGMPv3 "internet/igmpv3.html#pcapkit.protocols.internet.igmpv3.IGMP" + click IGMPv1 "internet/igmpv1.html#pcapkit.protocols.internet.igmpv1.IGMPv1" + click IGMPv2 "internet/igmpv2.html#pcapkit.protocols.internet.igmpv2.IGMPv2" + click IGMPv3 "internet/igmpv3.html#pcapkit.protocols.internet.igmpv3.IGMPv3" click HOPOPT "internet/hopopt.html#pcapkit.protocols.internet.hopopt.HOPOPT" click ICMP "internet/icmp.html#pcapkit.protocols.internet.icmp.ICMP" click ICMPv4 "internet/icmpv4.html#pcapkit.protocols.internet.icmpv4.ICMPv4" diff --git a/docs/source/pcapkit/protocols/internet/igmp.rst b/docs/source/pcapkit/protocols/internet/igmp.rst index 5aa84631d1..dc83af0c21 100644 --- a/docs/source/pcapkit/protocols/internet/igmp.rst +++ b/docs/source/pcapkit/protocols/internet/igmp.rst @@ -7,11 +7,11 @@ IGMP - Internet Group Management Protocol :class:`~pcapkit.protocols.internet.igmp.IGMP`, the abstract base of the Internet Group Management Protocol (IGMP) [*]_ versions 1, 2 and 3. IGMPv1 and IGMPv2 share the 8-octet structure below. -:class:`IGMPv1 ` and -:class:`IGMPv2 ` set +:class:`IGMPv1 ` and +:class:`IGMPv2 ` set :attr:`~pcapkit.protocols.internet.igmp.IGMP.version` and reuse this schema and data model as-is; IGMPv3's wire format differs materially by message, so -:class:`IGMPv3 ` carries its own +:class:`IGMPv3 ` carries its own instead -- see :doc:`igmpv3`. :attr:`~pcapkit.protocols.internet.igmp.IGMP.alias` names the protocol each message is read as: ``'IGMP'`` for IGMPv1, the name :rfc:`1112` gives it, and diff --git a/docs/source/pcapkit/protocols/internet/igmpv1.rst b/docs/source/pcapkit/protocols/internet/igmpv1.rst index 3734fceb2e..e5a1324826 100644 --- a/docs/source/pcapkit/protocols/internet/igmpv1.rst +++ b/docs/source/pcapkit/protocols/internet/igmpv1.rst @@ -4,13 +4,13 @@ IGMPv1 - Internet Group Management Protocol version 1 .. module:: pcapkit.protocols.internet.igmpv1 :mod:`pcapkit.protocols.internet.igmpv1` contains -:class:`~pcapkit.protocols.internet.igmpv1.IGMP`, which implements an +:class:`~pcapkit.protocols.internet.igmpv1.IGMPv1`, which implements an extractor for Internet Group Management Protocol version 1 (IGMPv1) [:rfc:`1112`]. It shares its header schema and data model with -:class:`IGMPv2 ` -- see +:class:`IGMPv2 ` -- see :mod:`~pcapkit.protocols.internet.igmp` for both. -.. autoclass:: pcapkit.protocols.internet.igmpv1.IGMP +.. autoclass:: pcapkit.protocols.internet.igmpv1.IGMPv1 :no-members: :show-inheritance: diff --git a/docs/source/pcapkit/protocols/internet/igmpv2.rst b/docs/source/pcapkit/protocols/internet/igmpv2.rst index 3ccee3c919..cd619e2136 100644 --- a/docs/source/pcapkit/protocols/internet/igmpv2.rst +++ b/docs/source/pcapkit/protocols/internet/igmpv2.rst @@ -4,13 +4,13 @@ IGMPv2 - Internet Group Management Protocol version 2 .. module:: pcapkit.protocols.internet.igmpv2 :mod:`pcapkit.protocols.internet.igmpv2` contains -:class:`~pcapkit.protocols.internet.igmpv2.IGMP`, which implements an +:class:`~pcapkit.protocols.internet.igmpv2.IGMPv2`, which implements an extractor for Internet Group Management Protocol version 2 (IGMPv2) [:rfc:`2236`]. It shares its header schema and data model with -:class:`IGMPv1 ` -- see +:class:`IGMPv1 ` -- see :mod:`~pcapkit.protocols.internet.igmp` for both. -.. autoclass:: pcapkit.protocols.internet.igmpv2.IGMP +.. autoclass:: pcapkit.protocols.internet.igmpv2.IGMPv2 :no-members: :show-inheritance: diff --git a/docs/source/pcapkit/protocols/internet/igmpv3.rst b/docs/source/pcapkit/protocols/internet/igmpv3.rst index 8bb01a0dff..96cac1bb7a 100644 --- a/docs/source/pcapkit/protocols/internet/igmpv3.rst +++ b/docs/source/pcapkit/protocols/internet/igmpv3.rst @@ -4,10 +4,10 @@ IGMPv3 - Internet Group Management Protocol version 3 .. module:: pcapkit.protocols.internet.igmpv3 :mod:`pcapkit.protocols.internet.igmpv3` contains -:class:`~pcapkit.protocols.internet.igmpv3.IGMP`, which implements an +:class:`~pcapkit.protocols.internet.igmpv3.IGMPv3`, which implements an extractor for Internet Group Management Protocol version 3 (IGMPv3) -[:rfc:`3376`]. Unlike :class:`IGMPv1 ` -and :class:`IGMPv2 `, which reuse the +[:rfc:`3376`]. Unlike :class:`IGMPv1 ` +and :class:`IGMPv2 `, which reuse the abstract base's own 8-octet schema and data model (see :mod:`~pcapkit.protocols.internet.igmp`), IGMPv3 defines two message shapes of its own: @@ -43,7 +43,7 @@ Octets Bits Name Description 8 64 ``igmp.body.records`` Group Record [1..M] ======= ========= ================================== =================================== -.. autoclass:: pcapkit.protocols.internet.igmpv3.IGMP +.. autoclass:: pcapkit.protocols.internet.igmpv3.IGMPv3 :no-members: :show-inheritance: @@ -63,7 +63,7 @@ Header Schemas .. module:: pcapkit.protocols.schema.internet.igmpv3 -.. autoclass:: pcapkit.protocols.schema.internet.igmpv3.IGMP +.. autoclass:: pcapkit.protocols.schema.internet.igmpv3.IGMPv3 :members: :show-inheritance: @@ -88,7 +88,7 @@ Data Models .. module:: pcapkit.protocols.data.internet.igmpv3 -.. autoclass:: pcapkit.protocols.data.internet.igmpv3.IGMP +.. autoclass:: pcapkit.protocols.data.internet.igmpv3.IGMPv3 :members: :show-inheritance: diff --git a/examples/generators/dispatch.py b/examples/generators/dispatch.py index 31a42ac97b..e69f1e92db 100644 --- a/examples/generators/dispatch.py +++ b/examples/generators/dispatch.py @@ -399,12 +399,12 @@ def _internet_payload(code: 'int') -> 'bytes': payload=_tcp(9999))) if code == TransType.IGMP: from pcapkit.const.igmp.type import Type - from pcapkit.protocols.internet.igmpv2 import IGMP + from pcapkit.protocols.internet.igmpv2 import IGMPv2 # A General Query: an IGMPv1/v2 message is the 8-octet header and # nothing else, so there is no further payload to wrap. A nonzero # max_resp_time makes it IGMPv2, the class IGMP dispatches it to. - return bytes(IGMP(type=Type.IGMP_Membership_Query, max_resp_time=100, group='0.0.0.0')) + return bytes(IGMPv2(type=Type.IGMP_Membership_Query, max_resp_time=100, group='0.0.0.0')) if code == TransType.IPv6_NoNxt: # The registered target for this code *is* Raw -- there is no # dissector to hand it, "no next header" is the whole point -- so any diff --git a/pcapkit/protocols/data/internet/__init__.py b/pcapkit/protocols/data/internet/__init__.py index 383e804934..8525089dca 100644 --- a/pcapkit/protocols/data/internet/__init__.py +++ b/pcapkit/protocols/data/internet/__init__.py @@ -126,7 +126,7 @@ # Internet Group Management Protocol from pcapkit.protocols.data.internet.igmp import IGMP from pcapkit.protocols.data.internet.igmpv3 import GroupRecord as IGMPv3_GroupRecord -from pcapkit.protocols.data.internet.igmpv3 import IGMP as IGMPv3 +from pcapkit.protocols.data.internet.igmpv3 import IGMPv3 from pcapkit.protocols.data.internet.igmpv3 import Query as IGMPv3_Query from pcapkit.protocols.data.internet.igmpv3 import Report as IGMPv3_Report diff --git a/pcapkit/protocols/data/internet/igmpv3.py b/pcapkit/protocols/data/internet/igmpv3.py index e1e652425f..09baaaf843 100644 --- a/pcapkit/protocols/data/internet/igmpv3.py +++ b/pcapkit/protocols/data/internet/igmpv3.py @@ -13,10 +13,10 @@ from pcapkit.const.igmp.record_type import RecordType as Enum_RecordType from pcapkit.const.igmp.type import Type as Enum_Type -__all__ = ['IGMP', 'Query', 'Report', 'GroupRecord'] +__all__ = ['IGMPv3', 'Query', 'Report', 'GroupRecord'] -class IGMP(Protocol): +class IGMPv3(Protocol): """Data model for Internet Group Management Protocol version 3. This is the common ancestor of the two message shapes IGMPv3 defines: @@ -31,17 +31,17 @@ class IGMP(Protocol): type: 'Enum_Type' #: Header length, in octets. IGMP has no header field of its own that #: states this -- unlike :class:`~pcapkit.protocols.internet.ah.AH`, it - #: is computed from what :meth:`IGMP.read - #: ` actually consumed, the + #: is computed from what :meth:`IGMPv3.read + #: ` actually consumed, the #: same way :attr:`~pcapkit.protocols.data.internet.esp.ESP.length` is. length: 'int' @info_final -class Query(IGMP): +class Query(IGMPv3): """Data model for an IGMPv3 Membership Query (:rfc:`3376#section-4.1`). - Reached only once :meth:`IGMP.read ` + Reached only once :meth:`IGMPv3.read ` has confirmed the message classifies as IGMPv3 (at least 12 octets), so the source-list extension fields below are always populated -- never :data:`None`, unlike the single-class design IGMPv1/v2 predate this one @@ -110,7 +110,7 @@ def __init__(self, record_type: 'Enum_RecordType', aux_data_len: 'int', num_sour @info_final -class Report(IGMP): +class Report(IGMPv3): """Data model for an IGMPv3 Membership Report (:rfc:`3376#section-4.2`).""" #: Reserved octet after :attr:`type` (:rfc:`3376#section-4.2.1`), kept as diff --git a/pcapkit/protocols/internet/__init__.py b/pcapkit/protocols/internet/__init__.py index 877122402e..2c434fc06f 100644 --- a/pcapkit/protocols/internet/__init__.py +++ b/pcapkit/protocols/internet/__init__.py @@ -19,9 +19,9 @@ from pcapkit.protocols.internet.esp import ESP from pcapkit.protocols.internet.icmpv4 import ICMPv4 from pcapkit.protocols.internet.icmpv6 import ICMPv6 -from pcapkit.protocols.internet.igmpv1 import IGMP as IGMPv1 -from pcapkit.protocols.internet.igmpv2 import IGMP as IGMPv2 -from pcapkit.protocols.internet.igmpv3 import IGMP as IGMPv3 +from pcapkit.protocols.internet.igmpv1 import IGMPv1 +from pcapkit.protocols.internet.igmpv2 import IGMPv2 +from pcapkit.protocols.internet.igmpv3 import IGMPv3 from pcapkit.protocols.internet.ipv4 import IPv4 from pcapkit.protocols.internet.ipv6 import IPv6 from pcapkit.protocols.internet.ipx import IPX diff --git a/pcapkit/protocols/internet/igmp.py b/pcapkit/protocols/internet/igmp.py index c7ea47b9c1..b8c6b5203a 100644 --- a/pcapkit/protocols/internet/igmp.py +++ b/pcapkit/protocols/internet/igmp.py @@ -8,9 +8,9 @@ :class:`~pcapkit.protocols.internet.igmp.IGMP`, the abstract base of IGMPv1 (:rfc:`1112`), IGMPv2 (:rfc:`2236`) and IGMPv3 (:rfc:`3376`) messages. IGMPv1 and IGMPv2 share one 8-octet layout, so the base holds that shared -schema, data model and read path. :class:`~pcapkit.protocols.internet.igmpv1.IGMP`, -:class:`~pcapkit.protocols.internet.igmpv2.IGMP` and -:class:`~pcapkit.protocols.internet.igmpv3.IGMP` set :attr:`version`; IGMPv3's +schema, data model and read path. :class:`~pcapkit.protocols.internet.igmpv1.IGMPv1`, +:class:`~pcapkit.protocols.internet.igmpv2.IGMPv2` and +:class:`~pcapkit.protocols.internet.igmpv3.IGMPv3` set :attr:`version`; IGMPv3's wire format differs materially (a Query's source-list extension, a Report's group-record list), so it carries its own schema and data model instead. The registry entry for IP protocol 2 is this base, and :meth:`IGMP._dispatch @@ -18,12 +18,6 @@ .. versionadded:: 1.6.0 - IGMPv1 (:rfc:`1112`) and IGMPv2 (:rfc:`2236`) (:issue:`2439`). - -.. versionadded:: 1.6.0 - - IGMPv3 (:rfc:`3376`) (:issue:`2448`). - """ import abc @@ -286,16 +280,16 @@ def _dispatch(cls, data: 'bytes', length: 'int') -> 'Type[ProtocolBase] | Type[R length: The IGMP message's length, in octets. Returns: - :class:`~pcapkit.protocols.internet.igmpv1.IGMP`, - :class:`~pcapkit.protocols.internet.igmpv2.IGMP` or - :class:`~pcapkit.protocols.internet.igmpv3.IGMP` for the version + :class:`~pcapkit.protocols.internet.igmpv1.IGMPv1`, + :class:`~pcapkit.protocols.internet.igmpv2.IGMPv2` or + :class:`~pcapkit.protocols.internet.igmpv3.IGMPv3` for the version the message classifies as; :class:`~pcapkit.protocols.misc.raw.Raw` for any other message, which then keeps its octets undissected. """ - from pcapkit.protocols.internet.igmpv1 import IGMP as IGMPv1 # isort: skip # pylint: disable=import-outside-toplevel - from pcapkit.protocols.internet.igmpv2 import IGMP as IGMPv2 # isort: skip # pylint: disable=import-outside-toplevel - from pcapkit.protocols.internet.igmpv3 import IGMP as IGMPv3 # isort: skip # pylint: disable=import-outside-toplevel + from pcapkit.protocols.internet.igmpv1 import IGMPv1 # isort: skip # pylint: disable=import-outside-toplevel + from pcapkit.protocols.internet.igmpv2 import IGMPv2 # isort: skip # pylint: disable=import-outside-toplevel + from pcapkit.protocols.internet.igmpv3 import IGMPv3 # isort: skip # pylint: disable=import-outside-toplevel from pcapkit.protocols.misc.raw import Raw # isort: skip # pylint: disable=import-outside-toplevel if len(data) < 2: diff --git a/pcapkit/protocols/internet/igmpv1.py b/pcapkit/protocols/internet/igmpv1.py index 86c33391d2..aa4e19748e 100644 --- a/pcapkit/protocols/internet/igmpv1.py +++ b/pcapkit/protocols/internet/igmpv1.py @@ -5,7 +5,7 @@ .. module:: pcapkit.protocols.internet.igmpv1 :mod:`pcapkit.protocols.internet.igmpv1` contains -:class:`~pcapkit.protocols.internet.igmpv1.IGMP`, which implements an +:class:`~pcapkit.protocols.internet.igmpv1.IGMPv1`, which implements an extractor for Internet Group Management Protocol version 1 (IGMPv1) [:rfc:`1112`]. It refuses a message of any other version, and a Membership Query with a non-zero ``max_resp_time`` is IGMPv2, not IGMPv1. @@ -22,11 +22,11 @@ if TYPE_CHECKING: from typing import Literal -__all__ = ['IGMP'] +__all__ = ['IGMPv1'] -class IGMP(IGMPBase[Data_IGMP, Schema_IGMP], - schema=Schema_IGMP, data=Data_IGMP): +class IGMPv1(IGMPBase[Data_IGMP, Schema_IGMP], + schema=Schema_IGMP, data=Data_IGMP): """This class implements Internet Group Management Protocol version 1.""" ########################################################################## diff --git a/pcapkit/protocols/internet/igmpv2.py b/pcapkit/protocols/internet/igmpv2.py index e8ec7658c3..6d07ab1a40 100644 --- a/pcapkit/protocols/internet/igmpv2.py +++ b/pcapkit/protocols/internet/igmpv2.py @@ -5,7 +5,7 @@ .. module:: pcapkit.protocols.internet.igmpv2 :mod:`pcapkit.protocols.internet.igmpv2` contains -:class:`~pcapkit.protocols.internet.igmpv2.IGMP`, which implements an +:class:`~pcapkit.protocols.internet.igmpv2.IGMPv2`, which implements an extractor for Internet Group Management Protocol version 2 (IGMPv2) [:rfc:`2236`]. It refuses a message of any other version; a Membership Query with a zero ``max_resp_time`` is IGMPv1, not IGMPv2. @@ -22,11 +22,11 @@ if TYPE_CHECKING: from typing import Literal -__all__ = ['IGMP'] +__all__ = ['IGMPv2'] -class IGMP(IGMPBase[Data_IGMP, Schema_IGMP], - schema=Schema_IGMP, data=Data_IGMP): +class IGMPv2(IGMPBase[Data_IGMP, Schema_IGMP], + schema=Schema_IGMP, data=Data_IGMP): """This class implements Internet Group Management Protocol version 2.""" ########################################################################## diff --git a/pcapkit/protocols/internet/igmpv3.py b/pcapkit/protocols/internet/igmpv3.py index d00dd44311..68f354c3ad 100644 --- a/pcapkit/protocols/internet/igmpv3.py +++ b/pcapkit/protocols/internet/igmpv3.py @@ -5,7 +5,7 @@ .. module:: pcapkit.protocols.internet.igmpv3 :mod:`pcapkit.protocols.internet.igmpv3` contains -:class:`~pcapkit.protocols.internet.igmpv3.IGMP`, which implements an +:class:`~pcapkit.protocols.internet.igmpv3.IGMPv3`, which implements an extractor for Internet Group Management Protocol version 3 (IGMPv3) [:rfc:`3376`]. Unlike IGMPv1/v2, IGMPv3 defines two message shapes of its own, both handled by this one class: @@ -47,20 +47,18 @@ .. versionadded:: 1.6.0 - IGMPv3 (:rfc:`3376`) (:issue:`2448`). - """ from typing import TYPE_CHECKING, cast from pcapkit.const.igmp.record_type import RecordType as Enum_RecordType from pcapkit.const.igmp.type import Type as Enum_Type -from pcapkit.protocols.data.internet.igmpv3 import IGMP as Data_IGMP from pcapkit.protocols.data.internet.igmpv3 import GroupRecord as Data_GroupRecord +from pcapkit.protocols.data.internet.igmpv3 import IGMPv3 as Data_IGMPv3 from pcapkit.protocols.data.internet.igmpv3 import Query as Data_Query from pcapkit.protocols.data.internet.igmpv3 import Report as Data_Report from pcapkit.protocols.internet.igmp import IGMP as IGMPBase -from pcapkit.protocols.schema.internet.igmpv3 import IGMP as Schema_IGMP from pcapkit.protocols.schema.internet.igmpv3 import GroupRecord as Schema_GroupRecord +from pcapkit.protocols.schema.internet.igmpv3 import IGMPv3 as Schema_IGMPv3 from pcapkit.protocols.schema.internet.igmpv3 import Query as Schema_Query from pcapkit.protocols.schema.internet.igmpv3 import QueryExtension as Schema_QueryExtension from pcapkit.protocols.schema.internet.igmpv3 import Report as Schema_Report @@ -77,7 +75,7 @@ from pcapkit.protocols.protocol import ProtocolBase from pcapkit.protocols.schema.schema import Schema -__all__ = ['IGMP'] +__all__ = ['IGMPv3'] def _decode_resp_code(code: 'int') -> 'int': @@ -141,12 +139,12 @@ def _encode_resp_code(value: 'int') -> 'int': return 0x80 | (exp << 4) | mant -class IGMP(IGMPBase[Data_IGMP, Schema_IGMP], - schema=Schema_IGMP, data=Data_IGMP): +class IGMPv3(IGMPBase[Data_IGMPv3, Schema_IGMPv3], + schema=Schema_IGMPv3, data=Data_IGMPv3): """This class implements Internet Group Management Protocol version 3. - Unlike :class:`~pcapkit.protocols.internet.igmpv1.IGMP` and - :class:`~pcapkit.protocols.internet.igmpv2.IGMP`, which share the base's + Unlike :class:`~pcapkit.protocols.internet.igmpv1.IGMPv1` and + :class:`~pcapkit.protocols.internet.igmpv2.IGMPv2`, which share the base's own 8-octet schema and data model, IGMPv3's wire format differs materially by message -- a Query's source-list extension, a Report's group-record list -- so this class carries its own (:issue:`2439`, @@ -170,7 +168,7 @@ def length(self) -> 'int': Every message starts with an 8-octet header. A Query may carry the source-list extension, and a Report's group-record list has no fixed size, so this is taken during :meth:`read` from the octets consumed - (see :attr:`~pcapkit.protocols.data.internet.igmpv3.IGMP.length`) + (see :attr:`~pcapkit.protocols.data.internet.igmpv3.IGMPv3.length`) rather than returned as a constant, unlike the base's own :attr:`~pcapkit.protocols.internet.igmp.IGMP.length`. @@ -193,7 +191,7 @@ def id(cls) -> 'tuple[Literal["IGMPv3"]]': # type: ignore[override] """ return ('IGMPv3',) - def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_IGMP': + def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_IGMPv3': """Read Internet Group Management Protocol version 3. Args: @@ -211,7 +209,7 @@ def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_IGMP': """ if length is None: length = len(self) - schema = cast('Schema_IGMP', self.__header__) + schema = cast('Schema_IGMPv3', self.__header__) body = schema.body if isinstance(body, Schema_Query): @@ -245,7 +243,7 @@ def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_IGMP': qqic=qqic, qqi=qqi, sources=sources, - ) # type: Data_IGMP + ) # type: Data_IGMPv3 elif isinstance(body, Schema_Report): records = tuple( Data_GroupRecord( @@ -291,7 +289,7 @@ def make(self, reserved_2: 'bytes' = b'\x00\x00', records: 'Optional[Iterable[dict[str, Any] | Data_GroupRecord]]' = None, payload: 'bytes | ProtocolBase | Schema' = b'', - **kwargs: 'Any') -> 'Schema_IGMP': + **kwargs: 'Any') -> 'Schema_IGMPv3': """Make (construct) packet data. Building a Query (the default) always writes the source-list @@ -362,7 +360,7 @@ def make(self, self._make_group_record(record) if not isinstance(record, dict) else self._make_group_record(**record) for record in records ] - return Schema_IGMP( + return Schema_IGMPv3( type=type_value, body=Schema_Report( reserved_1=reserved_1, @@ -389,7 +387,7 @@ def make(self, if max_resp_code is None: max_resp_code = _encode_resp_code(max_resp_time) - return Schema_IGMP( + return Schema_IGMPv3( type=type_value, body=Schema_Query( max_resp_code=max_resp_code, @@ -459,7 +457,7 @@ def _make_group_record(record: 'Optional[Data_GroupRecord]' = None, *, ########################################################################## @classmethod - def _make_data(cls, data: 'Data_IGMP') -> 'dict[str, Any]': # type: ignore[override] + def _make_data(cls, data: 'Data_IGMPv3') -> 'dict[str, Any]': # type: ignore[override] """Create key-value pairs from ``data`` for protocol construction. Args: diff --git a/pcapkit/protocols/internet/internet.py b/pcapkit/protocols/internet/internet.py index fa5029479b..82d2ceffd7 100644 --- a/pcapkit/protocols/internet/internet.py +++ b/pcapkit/protocols/internet/internet.py @@ -80,9 +80,9 @@ class Internet(ProtocolBase[_PT, _ST], Generic[_PT, _ST]): # pylint: disable=ab - :class:`pcapkit.protocols.internet.ah.AH` * - :attr:`~pcapkit.const.reg.transtype.TransType.IGMP` - :class:`pcapkit.protocols.internet.igmp.IGMP`, which dispatches to - :class:`pcapkit.protocols.internet.igmpv1.IGMP`, - :class:`pcapkit.protocols.internet.igmpv2.IGMP` or - :class:`pcapkit.protocols.internet.igmpv3.IGMP` + :class:`pcapkit.protocols.internet.igmpv1.IGMPv1`, + :class:`pcapkit.protocols.internet.igmpv2.IGMPv2` or + :class:`pcapkit.protocols.internet.igmpv3.IGMPv3` * - :attr:`~pcapkit.const.reg.transtype.TransType.IPv6_NoNxt` - :class:`pcapkit.protocols.misc.raw.Raw` * - :attr:`~pcapkit.const.reg.transtype.TransType.IPv6_Opts` diff --git a/pcapkit/protocols/schema/internet/__init__.py b/pcapkit/protocols/schema/internet/__init__.py index 5725c633fe..d167ab0a3d 100644 --- a/pcapkit/protocols/schema/internet/__init__.py +++ b/pcapkit/protocols/schema/internet/__init__.py @@ -131,7 +131,7 @@ # Internet Group Management Protocol from pcapkit.protocols.schema.internet.igmp import IGMP from pcapkit.protocols.schema.internet.igmpv3 import GroupRecord as IGMPv3_GroupRecord -from pcapkit.protocols.schema.internet.igmpv3 import IGMP as IGMPv3 +from pcapkit.protocols.schema.internet.igmpv3 import IGMPv3 from pcapkit.protocols.schema.internet.igmpv3 import Query as IGMPv3_Query from pcapkit.protocols.schema.internet.igmpv3 import QueryExtension as IGMPv3_QueryExtension from pcapkit.protocols.schema.internet.igmpv3 import Report as IGMPv3_Report diff --git a/pcapkit/protocols/schema/internet/igmpv3.py b/pcapkit/protocols/schema/internet/igmpv3.py index 90da9e68b9..7d97cf5e74 100644 --- a/pcapkit/protocols/schema/internet/igmpv3.py +++ b/pcapkit/protocols/schema/internet/igmpv3.py @@ -15,7 +15,7 @@ from pcapkit.protocols.schema.schema import Schema, schema_final from pcapkit.utilities.exceptions import ProtocolError, StreamEOFError -__all__ = ['IGMP', 'Query', 'QueryExtension', 'Report', 'GroupRecord'] +__all__ = ['IGMPv3', 'Query', 'QueryExtension', 'Report', 'GroupRecord'] if TYPE_CHECKING: from ipaddress import IPv4Address @@ -37,22 +37,22 @@ class QueryFlags(TypedDict): def igmpv3_body_selector(pkt: 'dict[str, Any]') -> 'SchemaField': - """Selector function for :attr:`IGMP.body` field. + """Selector function for :attr:`IGMPv3.body` field. Args: pkt: Packet data. Returns: Returns a :class:`~pcapkit.corekit.fields.misc.SchemaField` wrapped - :class:`Query` or :class:`Report` instance, chosen by :attr:`IGMP.type`. + :class:`Query` or :class:`Report` instance, chosen by :attr:`IGMPv3.type`. Raises: - ProtocolError: If :attr:`IGMP.type` is neither the Membership Query + ProtocolError: If :attr:`IGMPv3.type` is neither the Membership Query nor the IGMPv3 Membership Report. :meth:`IGMP._classify ` is what keeps a genuine IGMPv1/v2 message (an 8-octet Query, or a Report/Leave type) from ever reaching this class in the first place; this is - only reached when :class:`~pcapkit.protocols.internet.igmpv3.IGMP` + only reached when :class:`~pcapkit.protocols.internet.igmpv3.IGMPv3` is instantiated directly on a message of neither shape. """ @@ -107,7 +107,7 @@ class QueryExtension(Schema): 'qrv': (5, 3), }) #: Querier's Query Interval Code, as received on the wire; decoded into - #: seconds by :meth:`IGMP.read ` + #: seconds by :meth:`IGMPv3.read ` #: per :rfc:`3376#section-4.1.7`. qqic: 'int' = UInt8Field() #: Number of source addresses. @@ -120,8 +120,8 @@ class QueryExtension(Schema): #: Additional data (:rfc:`3376#section-4.1.10`): octets past the source #: list that a received Query may carry and that must be kept (to verify #: the checksum) but not otherwise acted on. Empty for a - #: standards-conformant Query, since :meth:`IGMP.make - #: ` never writes any. + #: standards-conformant Query, since :meth:`IGMPv3.make + #: ` never writes any. payload: 'bytes' = PayloadField() if TYPE_CHECKING: @@ -139,13 +139,13 @@ class Query(Schema): present only on a Query of 12 or more octets overall -- see :func:`query_extension_present`. An 8-octet Query, which this schema unpacks too (with :attr:`ext` :data:`None`), is not actually IGMPv3; - :meth:`IGMP.read ` is what + :meth:`IGMPv3.read ` is what refuses it. """ - #: Max response code; the raw octet :meth:`IGMP.read - #: ` decodes per + #: Max response code; the raw octet :meth:`IGMPv3.read + #: ` decodes per #: :rfc:`3376#section-4.1.1`. max_resp_code: 'int' = UInt8Field() #: Checksum: the 16-bit one's complement of the one's complement sum of @@ -289,7 +289,7 @@ def post_process(self, packet: 'dict[str, Any]') -> 'Schema': # pylint: disable @schema_final -class IGMP(Schema): +class IGMPv3(Schema): """Header schema for an IGMPv3 packet. :attr:`type` is common to both shapes IGMPv3 defines; everything past it @@ -315,14 +315,14 @@ class IGMP(Schema): def __init__(self, type: 'Enum_Type | int', body: 'Query | Report | bytes') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,redefined-builtin def get_payload(self, name: 'Optional[str]' = None) -> 'bytes': # pylint: disable=unused-argument - """Get payload of :class:`IGMP`. + """Get payload of :class:`IGMPv3`. Args: name: Unused; kept for signature compatibility with :meth:`Schema.get_payload `. Returns: - Trailing octets of :attr:`body` -- see the class :class:`Note ` + Trailing octets of :attr:`body` -- see the class :class:`Note ` above for exactly where each shape keeps them. """ diff --git a/tests/corekit/test_schema_roundtrip_unit.py b/tests/corekit/test_schema_roundtrip_unit.py index 876012bd72..55805de93b 100644 --- a/tests/corekit/test_schema_roundtrip_unit.py +++ b/tests/corekit/test_schema_roundtrip_unit.py @@ -77,7 +77,7 @@ 'internet.ipv6_opts:SMFHashBasedDPDOption': {'hav': b'\x80'}, # Type 0 is reserved, so the first member is no IGMP message at all; only # IGMPv3's own schema switches on it (the flat base/v1/v2 one does not). - 'internet.igmpv3:IGMP': {'type': lambda module: module.Enum_Type.IGMP_Membership_Query}, + 'internet.igmpv3:IGMPv3': {'type': lambda module: module.Enum_Type.IGMP_Membership_Query}, } # type: dict[str, dict[str, Any]] #: Packet context a dispatch target needs from its enclosing layer. diff --git a/tests/protocols/internet/test_igmp_unit.py b/tests/protocols/internet/test_igmp_unit.py index f0f8d82e06..7879e34ec4 100644 --- a/tests/protocols/internet/test_igmp_unit.py +++ b/tests/protocols/internet/test_igmp_unit.py @@ -3,11 +3,11 @@ GitHub issue #2439 implemented IGMPv1 (:rfc:`1112#appendix-I`) and IGMPv2 (:rfc:`2236#section-2`), which share one 8-octet message shape, as the -concrete :class:`~pcapkit.protocols.internet.igmpv1.IGMP` and -:class:`~pcapkit.protocols.internet.igmpv2.IGMP` siblings of the abstract +concrete :class:`~pcapkit.protocols.internet.igmpv1.IGMPv1` and +:class:`~pcapkit.protocols.internet.igmpv2.IGMPv2` siblings of the abstract :class:`~pcapkit.protocols.internet.igmp.IGMP` base. GitHub issue #2448 extends the same three-sibling pattern with a third, :class:`IGMPv3 -` (:rfc:`3376`): the Membership +` (:rfc:`3376`): the Membership Query's source-list extension and the Version 3 Membership Report's group-record list, both materially different shapes of their own. @@ -109,7 +109,7 @@ def igmp_class(version: 'str'): """Return the concrete class for ``version``: ``'IGMPv1'``, ``'IGMPv2'`` or ``'IGMPv3'``.""" from pcapkit.protocols.internet import ( # pylint: disable=import-outside-toplevel igmpv1, igmpv2, igmpv3) - return {'IGMPv1': igmpv1.IGMP, 'IGMPv2': igmpv2.IGMP, 'IGMPv3': igmpv3.IGMP}[version] + return {'IGMPv1': igmpv1.IGMPv1, 'IGMPv2': igmpv2.IGMPv2, 'IGMPv3': igmpv3.IGMPv3}[version] def expected_alias(version: 'str') -> 'str': @@ -198,14 +198,14 @@ def test_checksum_is_parsed_correctly(self) -> None: def test_type_and_group_are_parsed(self) -> None: from pcapkit.const.igmp.type import Type - from pcapkit.protocols.internet.igmpv2 import IGMP + from pcapkit.protocols.internet.igmpv2 import IGMPv2 - parsed = IGMP(io.BytesIO(IGMP_PACKETS['v2-report'][0]), 8) + parsed = IGMPv2(io.BytesIO(IGMP_PACKETS['v2-report'][0]), 8) self.assertEqual(parsed.info.type, Type.IGMPv2_Membership_Report) self.assertEqual(parsed.info.max_resp_time, 0) self.assertEqual(str(parsed.info.group), '239.1.2.3') - query = IGMP(io.BytesIO(IGMP_PACKETS['v2-query-general'][0]), 8) + query = IGMPv2(io.BytesIO(IGMP_PACKETS['v2-query-general'][0]), 8) self.assertEqual(query.info.type, Type.IGMP_Membership_Query) self.assertEqual(query.info.max_resp_time, 100) self.assertEqual(str(query.info.group), '0.0.0.0') @@ -216,23 +216,23 @@ def test_trailing_octets_are_kept_as_raw_and_rebuilt(self) -> None: :class:`~pcapkit.protocols.misc.raw.Raw`, as :meth:`ESP.read ` keeps its own trailer. """ - from pcapkit.protocols.internet.igmpv2 import IGMP + from pcapkit.protocols.internet.igmpv2 import IGMPv2 from pcapkit.protocols.misc.raw import Raw packet = IGMP_PACKETS['v2-report'][0] + b'XY' with warnings.catch_warnings(): warnings.simplefilter('ignore') - parsed = IGMP(io.BytesIO(packet), len(packet)) + parsed = IGMPv2(io.BytesIO(packet), len(packet)) self.assertIsInstance(parsed.payload, Raw) self.assertEqual(parsed.payload.info.packet, b'XY') - self.assertEqual(IGMP.from_data(parsed.info).data, packet) + self.assertEqual(IGMPv2.from_data(parsed.info).data, packet) def test_no_trailing_octets_have_no_payload(self) -> None: - from pcapkit.protocols.internet.igmpv1 import IGMP + from pcapkit.protocols.internet.igmpv1 import IGMPv1 from pcapkit.protocols.misc.null import NoPayload - parsed = IGMP(io.BytesIO(IGMP_PACKETS['v1-query'][0]), 8) + parsed = IGMPv1(io.BytesIO(IGMP_PACKETS['v1-query'][0]), 8) self.assertIsInstance(parsed.payload, NoPayload) def test_non_v1_v2_type_raises_protocol_error(self) -> None: @@ -242,8 +242,8 @@ def test_non_v1_v2_type_raises_protocol_error(self) -> None: the "reserved" and "number of group records" octets as a group address (#2439). """ - from pcapkit.protocols.internet.igmpv1 import IGMP as IGMPv1 - from pcapkit.protocols.internet.igmpv2 import IGMP as IGMPv2 + from pcapkit.protocols.internet.igmpv1 import IGMPv1 + from pcapkit.protocols.internet.igmpv2 import IGMPv2 from pcapkit.utilities.exceptions import ProtocolError for cls in (IGMPv1, IGMPv2): @@ -252,11 +252,11 @@ def test_non_v1_v2_type_raises_protocol_error(self) -> None: def test_non_v1_v2_type_dispatches_to_igmpv3(self) -> None: """Dispatched as an IPv4 payload, an IGMPv3 Membership Report must - reach :class:`~pcapkit.protocols.internet.igmpv3.IGMP`, not be + reach :class:`~pcapkit.protocols.internet.igmpv3.IGMPv3`, not be mis-decoded by IGMPv1/v2's 8-octet layout with ``group=0.0.0.1`` (#2439, #2448). """ - from pcapkit.protocols.internet.igmpv3 import IGMP as IGMPv3 + from pcapkit.protocols.internet.igmpv3 import IGMPv3 from pcapkit.protocols.internet.ipv4 import IPv4 made = IPv4(protocol=2, src='10.0.0.1', dst='224.0.0.1', payload=IGMPV3_REPORT) @@ -272,8 +272,8 @@ def test_non_8_octet_query_raises_protocol_error_on_v1_v2(self) -> None: 10-octet Query must be refused, not labelled ``IGMPv2`` on the strength of a nonzero Max Resp Code (#2439). """ - from pcapkit.protocols.internet.igmpv1 import IGMP as IGMPv1 - from pcapkit.protocols.internet.igmpv2 import IGMP as IGMPv2 + from pcapkit.protocols.internet.igmpv1 import IGMPv1 + from pcapkit.protocols.internet.igmpv2 import IGMPv2 from pcapkit.utilities.exceptions import ProtocolError for cls in (IGMPv1, IGMPv2): @@ -283,9 +283,9 @@ def test_non_8_octet_query_raises_protocol_error_on_v1_v2(self) -> None: def test_12_octet_query_dispatches_to_igmpv3(self) -> None: """Dispatched as an IPv4 payload, a 12-octet Query reaches - :class:`~pcapkit.protocols.internet.igmpv3.IGMP`, not ``IPv4:IGMPv2:Raw``. + :class:`~pcapkit.protocols.internet.igmpv3.IGMPv3`, not ``IPv4:IGMPv2:Raw``. """ - from pcapkit.protocols.internet.igmpv3 import IGMP as IGMPv3 + from pcapkit.protocols.internet.igmpv3 import IGMPv3 from pcapkit.protocols.internet.ipv4 import IPv4 packet = IPv4(protocol=2, src='10.0.0.1', dst='224.0.0.1', payload=IGMPV3_QUERY).data @@ -336,9 +336,9 @@ def test_alias_names_the_version_read(self) -> None: self.assertEqual(parsed.alias, expected_alias(version)) def test_refusals_name_the_version_refused(self) -> None: - from pcapkit.protocols.internet.igmpv1 import IGMP as IGMPv1 - from pcapkit.protocols.internet.igmpv2 import IGMP as IGMPv2 - from pcapkit.protocols.internet.igmpv3 import IGMP as IGMPv3 + from pcapkit.protocols.internet.igmpv1 import IGMPv1 + from pcapkit.protocols.internet.igmpv2 import IGMPv2 + from pcapkit.protocols.internet.igmpv3 import IGMPv3 from pcapkit.utilities.exceptions import ProtocolError v1_report, v2_report = IGMP_PACKETS['v1-report'][0], IGMP_PACKETS['v2-report'][0] @@ -356,17 +356,17 @@ def test_refusals_name_the_version_refused(self) -> None: IGMPv3(io.BytesIO(v2_query), len(v2_query)) def test_membership_query_max_resp_time_zero_is_v1(self) -> None: - from pcapkit.protocols.internet.igmpv1 import IGMP + from pcapkit.protocols.internet.igmpv1 import IGMPv1 packet = igmp_packet(0x11, 0, '0.0.0.0') - parsed = IGMP(io.BytesIO(packet), len(packet)) + parsed = IGMPv1(io.BytesIO(packet), len(packet)) self.assertEqual(parsed.alias, 'IGMP') def test_membership_query_max_resp_time_nonzero_is_v2(self) -> None: - from pcapkit.protocols.internet.igmpv2 import IGMP + from pcapkit.protocols.internet.igmpv2 import IGMPv2 packet = igmp_packet(0x11, 1, '0.0.0.0') - parsed = IGMP(io.BytesIO(packet), len(packet)) + parsed = IGMPv2(io.BytesIO(packet), len(packet)) self.assertEqual(parsed.alias, 'IGMPv2') def test_classify_rejects_a_non_igmp_type(self) -> None: @@ -418,9 +418,9 @@ def test_base_is_abstract(self) -> None: IGMP(io.BytesIO(packet), len(packet)) # type: ignore[abstract] def test_id_and_version(self) -> None: - from pcapkit.protocols.internet.igmpv1 import IGMP as IGMPv1 - from pcapkit.protocols.internet.igmpv2 import IGMP as IGMPv2 - from pcapkit.protocols.internet.igmpv3 import IGMP as IGMPv3 + from pcapkit.protocols.internet.igmpv1 import IGMPv1 + from pcapkit.protocols.internet.igmpv2 import IGMPv2 + from pcapkit.protocols.internet.igmpv3 import IGMPv3 self.assertEqual(IGMPv1.id(), ('IGMP',)) self.assertEqual(IGMPv2.id(), ('IGMPv2',)) @@ -453,8 +453,28 @@ def test_each_version_narrows_the_base_id(self) -> None: self.assertNotIn('IGMP', v3.id()) self.assertNotIn('IGMPv2', v3.id()) + def test_each_class_takes_its_module_s_name(self) -> None: + """A protocol, schema or data class is named after its module, whatever + its ``id()``, so IGMPv1 is ``IGMPv1`` though its ``id()`` is + ``('IGMP',)`` (#2529). The data model key follows the class name.""" + import importlib + + for module, name in (('internet.igmpv1', 'IGMPv1'), ('internet.igmpv2', 'IGMPv2'), + ('internet.igmpv3', 'IGMPv3'), ('schema.internet.igmpv3', 'IGMPv3'), + ('data.internet.igmpv3', 'IGMPv3')): + mod = importlib.import_module(f'pcapkit.protocols.{module}') + with self.subTest(module=mod.__name__): + self.assertEqual(getattr(mod, name).__name__, name) + self.assertEqual(mod.__all__[0], name) + self.assertNotIn('IGMP', mod.__all__) + self.assertEqual(igmp_class('IGMPv1').id(), ('IGMP',)) + for name, (packet, version) in {**IGMP_PACKETS, **IGMPV3_PACKETS}.items(): + with self.subTest(packet=name): + parsed = igmp_class(version)(io.BytesIO(packet), len(packet)) + self.assertEqual(parsed.info_name, version.lower()) + def test_igmpv1_parses_its_own_messages_directly(self) -> None: - from pcapkit.protocols.internet.igmpv1 import IGMP as IGMPv1 + from pcapkit.protocols.internet.igmpv1 import IGMPv1 for name in ('v1-query', 'v1-report'): packet, _version = IGMP_PACKETS[name] @@ -463,7 +483,7 @@ def test_igmpv1_parses_its_own_messages_directly(self) -> None: self.assertEqual(parsed.alias, 'IGMP') def test_igmpv1_rejects_a_v2_only_message(self) -> None: - from pcapkit.protocols.internet.igmpv1 import IGMP as IGMPv1 + from pcapkit.protocols.internet.igmpv1 import IGMPv1 from pcapkit.utilities.exceptions import ProtocolError packet, _version = IGMP_PACKETS['v2-report'] @@ -471,7 +491,7 @@ def test_igmpv1_rejects_a_v2_only_message(self) -> None: IGMPv1(io.BytesIO(packet), len(packet)) def test_igmpv1_rejects_a_query_with_nonzero_max_resp_time(self) -> None: - from pcapkit.protocols.internet.igmpv1 import IGMP as IGMPv1 + from pcapkit.protocols.internet.igmpv1 import IGMPv1 from pcapkit.utilities.exceptions import ProtocolError packet, _version = IGMP_PACKETS['v2-query-general'] @@ -479,7 +499,7 @@ def test_igmpv1_rejects_a_query_with_nonzero_max_resp_time(self) -> None: IGMPv1(io.BytesIO(packet), len(packet)) def test_igmpv2_parses_its_own_messages_directly(self) -> None: - from pcapkit.protocols.internet.igmpv2 import IGMP as IGMPv2 + from pcapkit.protocols.internet.igmpv2 import IGMPv2 for name in ('v2-query-general', 'v2-query-group', 'v2-report', 'v2-leave'): packet, _version = IGMP_PACKETS[name] @@ -488,7 +508,7 @@ def test_igmpv2_parses_its_own_messages_directly(self) -> None: self.assertEqual(parsed.alias, 'IGMPv2') def test_igmpv2_rejects_a_v1_only_message(self) -> None: - from pcapkit.protocols.internet.igmpv2 import IGMP as IGMPv2 + from pcapkit.protocols.internet.igmpv2 import IGMPv2 from pcapkit.utilities.exceptions import ProtocolError packet, _version = IGMP_PACKETS['v1-report'] @@ -496,7 +516,7 @@ def test_igmpv2_rejects_a_v1_only_message(self) -> None: IGMPv2(io.BytesIO(packet), len(packet)) def test_igmpv2_rejects_a_query_with_zero_max_resp_time(self) -> None: - from pcapkit.protocols.internet.igmpv2 import IGMP as IGMPv2 + from pcapkit.protocols.internet.igmpv2 import IGMPv2 from pcapkit.utilities.exceptions import ProtocolError packet, _version = IGMP_PACKETS['v1-query'] @@ -507,7 +527,7 @@ def test_igmpv2_rejects_a_non_8_octet_query(self) -> None: """A 12-octet IGMPv3 General Query or a 10-octet Query is refused by the IGMPv2 class, as a v1 message is (:rfc:`3376#section-7.1`). """ - from pcapkit.protocols.internet.igmpv2 import IGMP as IGMPv2 + from pcapkit.protocols.internet.igmpv2 import IGMPv2 from pcapkit.utilities.exceptions import ProtocolError for name, packet in (('v3-query', IGMPV3_QUERY), ('10-octet-query', IGMP_QUERY_10_OCTETS)): @@ -518,7 +538,7 @@ def test_igmpv1_rejects_a_non_8_octet_query(self) -> None: """Likewise for the IGMPv1 class, with a zero Max Resp Code so the length is the only thing refusing it (:rfc:`3376#section-7.1`). """ - from pcapkit.protocols.internet.igmpv1 import IGMP as IGMPv1 + from pcapkit.protocols.internet.igmpv1 import IGMPv1 from pcapkit.utilities.exceptions import ProtocolError for tail in (bytes(4), bytes(2)): @@ -527,7 +547,7 @@ def test_igmpv1_rejects_a_non_8_octet_query(self) -> None: IGMPv1(io.BytesIO(packet), len(packet)) def test_igmpv3_parses_its_own_messages_directly(self) -> None: - from pcapkit.protocols.internet.igmpv3 import IGMP as IGMPv3 + from pcapkit.protocols.internet.igmpv3 import IGMPv3 for name, (packet, _version) in IGMPV3_PACKETS.items(): with self.subTest(packet=name): @@ -535,7 +555,7 @@ def test_igmpv3_parses_its_own_messages_directly(self) -> None: self.assertEqual(parsed.alias, 'IGMPv3') def test_igmpv3_rejects_an_8_octet_v1_or_v2_message(self) -> None: - from pcapkit.protocols.internet.igmpv3 import IGMP as IGMPv3 + from pcapkit.protocols.internet.igmpv3 import IGMPv3 from pcapkit.utilities.exceptions import ProtocolError for name in ('v1-query', 'v1-report', 'v2-query-general', 'v2-report', 'v2-leave'): @@ -547,7 +567,7 @@ def test_igmpv3_rejects_a_9_to_11_octet_query(self) -> None: """Neither IGMPv1/v2 (8 octets) nor IGMPv3 (12 or more) claims a Membership Query of 9 to 11 octets; the IGMPv3 class refuses it too. """ - from pcapkit.protocols.internet.igmpv3 import IGMP as IGMPv3 + from pcapkit.protocols.internet.igmpv3 import IGMPv3 from pcapkit.utilities.exceptions import ProtocolError with self.assertRaises(ProtocolError): @@ -557,8 +577,8 @@ def test_versions_take_the_length_from_the_data_when_none_is_given(self) -> None """Without a ``length``, each version reads the whole of ``bytes`` data: trailing octets become :class:`~pcapkit.protocols.misc.raw.Raw`. """ - from pcapkit.protocols.internet.igmpv1 import IGMP as IGMPv1 - from pcapkit.protocols.internet.igmpv2 import IGMP as IGMPv2 + from pcapkit.protocols.internet.igmpv1 import IGMPv1 + from pcapkit.protocols.internet.igmpv2 import IGMPv2 from pcapkit.protocols.misc.raw import Raw cases = ( @@ -579,7 +599,7 @@ def test_versions_take_the_length_from_the_data_when_none_is_given(self) -> None self.assertEqual(trailed.payload.info.packet, b'XY') def test_igmpv3_takes_the_length_from_the_data_when_none_is_given(self) -> None: - from pcapkit.protocols.internet.igmpv3 import IGMP as IGMPv3 + from pcapkit.protocols.internet.igmpv3 import IGMPv3 from pcapkit.protocols.misc.raw import Raw packet, _version = IGMPV3_PACKETS['v3-query'] @@ -603,7 +623,7 @@ def setUp(self) -> None: def test_igmp_index_length_and_name(self) -> None: from pcapkit.const.reg.transtype import TransType from pcapkit.protocols.internet.igmp import IGMP - from pcapkit.protocols.internet.igmpv2 import IGMP as IGMPv2 + from pcapkit.protocols.internet.igmpv2 import IGMPv2 proto = object.__new__(IGMPv2) @@ -614,7 +634,7 @@ def test_igmp_index_length_and_name(self) -> None: self.assertEqual(proto.alias, 'IGMPv2') def test_igmpv3_length_varies_with_the_message(self) -> None: - from pcapkit.protocols.internet.igmpv3 import IGMP as IGMPv3 + from pcapkit.protocols.internet.igmpv3 import IGMPv3 for name, (packet, _version) in IGMPV3_PACKETS.items(): with self.subTest(packet=name): @@ -645,9 +665,9 @@ class DummyDict(dict): def test_make_builds_schema(self) -> None: from pcapkit.const.igmp.type import Type - from pcapkit.protocols.internet.igmpv1 import IGMP + from pcapkit.protocols.internet.igmpv1 import IGMPv1 - proto = object.__new__(IGMP) + proto = object.__new__(IGMPv1) schema = proto.make( type=Type.IGMPv1_Membership_Report, group='224.0.0.1', @@ -659,10 +679,10 @@ def test_make_builds_schema(self) -> None: self.assertEqual(bytes(schema)[4:8], ipaddress.ip_address('224.0.0.1').packed) def test_make_rejects_out_of_range_octets(self) -> None: - from pcapkit.protocols.internet.igmpv1 import IGMP + from pcapkit.protocols.internet.igmpv1 import IGMPv1 from pcapkit.utilities.exceptions import ProtocolError - proto = object.__new__(IGMP) + proto = object.__new__(IGMPv1) for field in ('type', 'max_resp_time'): for value in (256, -1): with self.subTest(field=field, value=value), \ @@ -670,20 +690,20 @@ def test_make_rejects_out_of_range_octets(self) -> None: proto.make(**{field: value}) def test_make_rejects_non_int_max_resp_time(self) -> None: - from pcapkit.protocols.internet.igmpv1 import IGMP + from pcapkit.protocols.internet.igmpv1 import IGMPv1 from pcapkit.utilities.exceptions import ProtocolError - proto = object.__new__(IGMP) + proto = object.__new__(IGMPv1) for value in (1.5, '3', None, True, False): with self.subTest(value=value), \ self.assertRaisesRegex(ProtocolError, 'invalid max_resp_time'): proto.make(max_resp_time=value) # type: ignore[arg-type] def test_make_rejects_bool_type(self) -> None: - from pcapkit.protocols.internet.igmpv1 import IGMP + from pcapkit.protocols.internet.igmpv1 import IGMPv1 from pcapkit.utilities.exceptions import ProtocolError - proto = object.__new__(IGMP) + proto = object.__new__(IGMPv1) for value in (True, False): with self.subTest(value=value), \ self.assertRaisesRegex(ProtocolError, 'invalid type'): @@ -780,10 +800,10 @@ def test_query_with_max_resp_code_0x8c_decodes_max_resp_time_224(self) -> None: what reading ``0x8c`` as a plain tenths-of-a-second integer (140) would give. """ - from pcapkit.protocols.internet.igmpv3 import IGMP + from pcapkit.protocols.internet.igmpv3 import IGMPv3 packet = igmpv3_query(0x8c, '239.1.2.3', qqic=0x8c) - parsed = IGMP(io.BytesIO(packet), len(packet)) + parsed = IGMPv3(io.BytesIO(packet), len(packet)) self.assertEqual(parsed.info.max_resp_code, 0x8c) self.assertEqual(parsed.info.max_resp_time, 224) @@ -791,47 +811,47 @@ def test_query_with_max_resp_code_0x8c_decodes_max_resp_time_224(self) -> None: self.assertEqual(parsed.info.qqic, 0x8c) self.assertEqual(parsed.info.qqi, 224) - self.assertEqual(IGMP.from_data(parsed.info).data, packet) + self.assertEqual(IGMPv3.from_data(parsed.info).data, packet) def test_8_octet_query_max_resp_code_200_reads_200_not_3072(self) -> None: """An IGMPv2 Query has no extension, so its code is literal: 200 is 20.0 seconds. Read as floating-point it would be 3072. """ - from pcapkit.protocols.internet.igmpv2 import IGMP + from pcapkit.protocols.internet.igmpv2 import IGMPv2 packet = igmp_packet(0x11, 200, '239.1.2.3') - parsed = IGMP(io.BytesIO(packet), len(packet)) + parsed = IGMPv2(io.BytesIO(packet), len(packet)) self.assertEqual(parsed.info.max_resp_time, 200) - self.assertEqual(IGMP.from_data(parsed.info).data, packet) + self.assertEqual(IGMPv2.from_data(parsed.info).data, packet) def test_12_octet_query_max_resp_code_200_decodes_the_float(self) -> None: """A 12-octet IGMPv3 Query carries the extension, so code 200 (``0xc8``: exp 4, mant 8) decodes to ``(8 | 0x10) << 7 = 3072``. """ - from pcapkit.protocols.internet.igmpv3 import IGMP + from pcapkit.protocols.internet.igmpv3 import IGMPv3 packet = igmpv3_query(200, '239.1.2.3') self.assertEqual(len(packet), 12) - parsed = IGMP(io.BytesIO(packet), len(packet)) + parsed = IGMPv3(io.BytesIO(packet), len(packet)) self.assertEqual(parsed.info.max_resp_time, 3072) - self.assertEqual(IGMP.from_data(parsed.info).data, packet) + self.assertEqual(IGMPv3.from_data(parsed.info).data, packet) def test_make_v3_query_floating_point_encodes_max_resp_time_200(self) -> None: """``make()`` on an IGMPv3 Query encodes ``max_resp_time`` 200 as ``0x89`` (``(9 | 0x10) << 3 = 200``), and parsing that octet gives 200 back. """ - from pcapkit.protocols.internet.igmpv3 import IGMP, _encode_resp_code + from pcapkit.protocols.internet.igmpv3 import IGMPv3, _encode_resp_code self.assertEqual(_encode_resp_code(200), 0x89) - proto = object.__new__(IGMP) + proto = object.__new__(IGMPv3) packet = bytes(proto.make(max_resp_time=200)) self.assertEqual(len(packet), 12) self.assertEqual(packet[1], 0x89) - parsed = IGMP(io.BytesIO(packet), len(packet)) + parsed = IGMPv3(io.BytesIO(packet), len(packet)) self.assertEqual(parsed.info.max_resp_time, 200) @@ -842,10 +862,10 @@ def setUp(self) -> None: reimport_once_per_class(self) def test_general_query_with_no_sources_round_trips(self) -> None: - from pcapkit.protocols.internet.igmpv3 import IGMP + from pcapkit.protocols.internet.igmpv3 import IGMPv3 packet = igmpv3_query(100, '0.0.0.0', s_flag=0, qrv=2, qqic=125) - parsed = IGMP(io.BytesIO(packet), len(packet)) + parsed = IGMPv3(io.BytesIO(packet), len(packet)) self.assertEqual(parsed.info.max_resp_time, 100) self.assertEqual(str(parsed.info.group), '0.0.0.0') @@ -854,14 +874,14 @@ def test_general_query_with_no_sources_round_trips(self) -> None: self.assertEqual(parsed.info.qqi, 125) self.assertEqual(parsed.info.sources, ()) - self.assertEqual(IGMP.from_data(parsed.info).data, packet) + self.assertEqual(IGMPv3.from_data(parsed.info).data, packet) def test_group_and_source_specific_query_round_trips(self) -> None: - from pcapkit.protocols.internet.igmpv3 import IGMP + from pcapkit.protocols.internet.igmpv3 import IGMPv3 sources = ('10.0.0.1', '10.0.0.2', '10.0.0.3') packet = igmpv3_query(50, '239.5.5.5', s_flag=1, qrv=5, qqic=60, sources=sources) - parsed = IGMP(io.BytesIO(packet), len(packet)) + parsed = IGMPv3(io.BytesIO(packet), len(packet)) self.assertEqual(str(parsed.info.group), '239.5.5.5') self.assertIs(parsed.info.s_flag, True) @@ -872,7 +892,7 @@ def test_group_and_source_specific_query_round_trips(self) -> None: sources, ) - self.assertEqual(IGMP.from_data(parsed.info).data, packet) + self.assertEqual(IGMPv3.from_data(parsed.info).data, packet) def test_make_defaults_build_a_12_octet_general_query(self) -> None: """Unlike the base's own :meth:`~pcapkit.protocols.internet.igmp.IGMP.make`, @@ -880,9 +900,9 @@ def test_make_defaults_build_a_12_octet_general_query(self) -> None: so there is no plain-8-octet default to fall back to. """ from pcapkit.const.igmp.type import Type - from pcapkit.protocols.internet.igmpv3 import IGMP + from pcapkit.protocols.internet.igmpv3 import IGMPv3 - proto = object.__new__(IGMP) + proto = object.__new__(IGMPv3) schema = proto.make(type=Type.IGMP_Membership_Query, group='0.0.0.0') self.assertIsNotNone(schema.body.ext) @@ -890,9 +910,9 @@ def test_make_defaults_build_a_12_octet_general_query(self) -> None: def test_qqi_alone_is_honoured(self) -> None: from pcapkit.const.igmp.type import Type - from pcapkit.protocols.internet.igmpv3 import IGMP + from pcapkit.protocols.internet.igmpv3 import IGMPv3 - proto = object.__new__(IGMP) + proto = object.__new__(IGMPv3) schema = proto.make(type=Type.IGMP_Membership_Query, group='0.0.0.0', qqi=125) self.assertEqual(schema.body.ext.qqic, 125) @@ -901,29 +921,29 @@ def test_additional_data_after_the_source_list_is_kept_as_payload(self) -> None: """:rfc:`3376#section-4.1.10` permits octets past the source list ("Additional Data"). They are kept as payload, outside the source list, so they count toward neither - :attr:`~pcapkit.protocols.internet.igmpv3.IGMP.length` nor the + :attr:`~pcapkit.protocols.internet.igmpv3.IGMPv3.length` nor the source list, and the rebuild reproduces them. """ - from pcapkit.protocols.internet.igmpv3 import IGMP + from pcapkit.protocols.internet.igmpv3 import IGMPv3 from pcapkit.protocols.misc.raw import Raw packet = igmpv3_query(0x7d, '239.1.2.3', s_flag=0, qrv=0, qqic=0x7d, sources=('10.0.0.1',)) + b'\xca\xfe' with warnings.catch_warnings(): warnings.simplefilter('ignore') - parsed = IGMP(io.BytesIO(packet), len(packet)) + parsed = IGMPv3(io.BytesIO(packet), len(packet)) self.assertIsInstance(parsed.payload, Raw) self.assertEqual(parsed.payload.info.packet, b'\xca\xfe') self.assertEqual(parsed.info.length, len(packet) - 2) - self.assertEqual(IGMP.from_data(parsed.info).data, packet) + self.assertEqual(IGMPv3.from_data(parsed.info).data, packet) def test_resv_nibble_round_trips(self) -> None: """The Resv nibble (:rfc:`3376#section-4.1.4`) is read into the data model, so a non-conformant sender's value survives the rebuild instead of being rewritten to zero. """ - from pcapkit.protocols.internet.igmpv3 import IGMP + from pcapkit.protocols.internet.igmpv3 import IGMPv3 packet = bytearray(igmpv3_query(0x7d, '239.1.2.3', s_flag=1, qrv=3, qqic=0x7d)) # Byte 8 is Resv(4)/S(1)/QRV(3); flip Resv from 0 to 0xA (1010). @@ -932,9 +952,9 @@ def test_resv_nibble_round_trips(self) -> None: packet[2:4] = checksum packet = bytes(packet) - parsed = IGMP(io.BytesIO(packet), len(packet)) + parsed = IGMPv3(io.BytesIO(packet), len(packet)) self.assertEqual(parsed.info.resv, 0xA) - self.assertEqual(IGMP.from_data(parsed.info).data, packet) + self.assertEqual(IGMPv3.from_data(parsed.info).data, packet) class IGMPv3ReportTests(unittest.TestCase): @@ -945,23 +965,23 @@ def setUp(self) -> None: def test_report_with_no_group_records_round_trips(self) -> None: from pcapkit.const.igmp.type import Type - from pcapkit.protocols.internet.igmpv3 import IGMP + from pcapkit.protocols.internet.igmpv3 import IGMPv3 packet = igmpv3_report(()) - parsed = IGMP(io.BytesIO(packet), len(packet)) + parsed = IGMPv3(io.BytesIO(packet), len(packet)) self.assertEqual(parsed.info.type, Type.IGMPv3_Membership_Report) self.assertEqual(parsed.info.num_group_records, 0) self.assertEqual(parsed.info.records, ()) - self.assertEqual(IGMP.from_data(parsed.info).data, packet) + self.assertEqual(IGMPv3.from_data(parsed.info).data, packet) def test_report_with_one_record_of_each_type_round_trips(self) -> None: """All six :rfc:`3376#section-4.2.12` group record types, one record each, with a mix of zero, one and multiple source addresses. """ from pcapkit.const.igmp.record_type import RecordType - from pcapkit.protocols.internet.igmpv3 import IGMP + from pcapkit.protocols.internet.igmpv3 import IGMPv3 records = ( ('MODE_IS_INCLUDE', 1, '224.0.1.1', ()), @@ -975,7 +995,7 @@ def test_report_with_one_record_of_each_type_round_trips(self) -> None: igmpv3_group_record(code, mcast, sources) for _, code, mcast, sources in records ) packet = igmpv3_report(raw_records) - parsed = IGMP(io.BytesIO(packet), len(packet)) + parsed = IGMPv3(io.BytesIO(packet), len(packet)) self.assertEqual(parsed.info.num_group_records, 6) self.assertEqual(len(parsed.info.records), 6) @@ -992,21 +1012,21 @@ def test_report_with_one_record_of_each_type_round_trips(self) -> None: self.assertEqual(record.aux_data, b'') self.assertEqual(record.aux_data_len, 0) - self.assertEqual(IGMP.from_data(parsed.info).data, packet) + self.assertEqual(IGMPv3.from_data(parsed.info).data, packet) def test_report_with_multiple_records_for_the_same_group(self) -> None: """:rfc:`3376#section-4.2.12`: a source-list change that both adds and removes sources is sent as two records for the same multicast address, one ``ALLOW_NEW_SOURCES`` and one ``BLOCK_OLD_SOURCES``. """ - from pcapkit.protocols.internet.igmpv3 import IGMP + from pcapkit.protocols.internet.igmpv3 import IGMPv3 raw_records = ( igmpv3_group_record(5, '239.9.9.9', ('10.1.1.1',)), igmpv3_group_record(6, '239.9.9.9', ('10.1.1.2',)), ) packet = igmpv3_report(raw_records) - parsed = IGMP(io.BytesIO(packet), len(packet)) + parsed = IGMPv3(io.BytesIO(packet), len(packet)) self.assertEqual(len(parsed.info.records), 2) self.assertEqual(str(parsed.info.records[0].multicast_address), '239.9.9.9') @@ -1014,23 +1034,23 @@ def test_report_with_multiple_records_for_the_same_group(self) -> None: self.assertEqual(parsed.info.records[0].record_type, 5) self.assertEqual(parsed.info.records[1].record_type, 6) - self.assertEqual(IGMP.from_data(parsed.info).data, packet) + self.assertEqual(IGMPv3.from_data(parsed.info).data, packet) def test_unrecognized_record_type_is_kept_not_rejected(self) -> None: """:rfc:`3376#section-4.2.12`: "Unrecognized Record Type values MUST be silently ignored" -- i.e. parsing continues and the value is simply not one of the six named ones, rather than raising. """ - from pcapkit.protocols.internet.igmpv3 import IGMP + from pcapkit.protocols.internet.igmpv3 import IGMPv3 raw_records = (igmpv3_group_record(200, '224.0.2.1'),) packet = igmpv3_report(raw_records) - parsed = IGMP(io.BytesIO(packet), len(packet)) + parsed = IGMPv3(io.BytesIO(packet), len(packet)) self.assertEqual(len(parsed.info.records), 1) self.assertEqual(int(parsed.info.records[0].record_type), 200) - self.assertEqual(IGMP.from_data(parsed.info).data, packet) + self.assertEqual(IGMPv3.from_data(parsed.info).data, packet) def test_additional_data_after_the_last_record_is_kept_as_payload(self) -> None: """:rfc:`3376#section-4.2.11` permits octets past the last group @@ -1039,28 +1059,28 @@ def test_additional_data_after_the_last_record_is_kept_as_payload(self) -> None: in octets, so those octets are trimmed back out and kept as this message's trailing payload rather than read as a further record. """ - from pcapkit.protocols.internet.igmpv3 import IGMP + from pcapkit.protocols.internet.igmpv3 import IGMPv3 from pcapkit.protocols.misc.raw import Raw raw_records = (igmpv3_group_record(1, '224.0.1.1'),) packet = igmpv3_report(raw_records) + b'\xca\xfe' with warnings.catch_warnings(): warnings.simplefilter('ignore') - parsed = IGMP(io.BytesIO(packet), len(packet)) + parsed = IGMPv3(io.BytesIO(packet), len(packet)) self.assertEqual(parsed.info.num_group_records, 1) self.assertEqual(len(parsed.info.records), 1) self.assertIsInstance(parsed.payload, Raw) self.assertEqual(parsed.payload.info.packet, b'\xca\xfe') self.assertEqual(parsed.info.length, len(packet) - 2) - self.assertEqual(IGMP.from_data(parsed.info).data, packet) + self.assertEqual(IGMPv3.from_data(parsed.info).data, packet) def test_reserved_fields_round_trip(self) -> None: """The two Reserved fields (:rfc:`3376#section-4.2.1`) are read into the data model, so a non-conformant sender's values survive the rebuild. """ - from pcapkit.protocols.internet.igmpv3 import IGMP + from pcapkit.protocols.internet.igmpv3 import IGMPv3 raw_records = (igmpv3_group_record(1, '224.0.1.1'),) packet = bytearray(igmpv3_report(raw_records)) @@ -1070,17 +1090,17 @@ def test_reserved_fields_round_trip(self) -> None: packet[2:4] = internet_checksum(zeroed) packet = bytes(packet) - parsed = IGMP(io.BytesIO(packet), len(packet)) + parsed = IGMPv3(io.BytesIO(packet), len(packet)) self.assertEqual(parsed.info.reserved_1, 0xFF) self.assertEqual(parsed.info.reserved_2, b'\xbe\xef') - self.assertEqual(IGMP.from_data(parsed.info).data, packet) + self.assertEqual(IGMPv3.from_data(parsed.info).data, packet) def test_make_builds_report_from_dict_records(self) -> None: from pcapkit.const.igmp.record_type import RecordType from pcapkit.const.igmp.type import Type - from pcapkit.protocols.internet.igmpv3 import IGMP + from pcapkit.protocols.internet.igmpv3 import IGMPv3 - proto = object.__new__(IGMP) + proto = object.__new__(IGMPv3) schema = proto.make( type=Type.IGMPv3_Membership_Report, records=[ @@ -1130,9 +1150,9 @@ def test_base_refuses_instantiation(self) -> None: IGMP(io.BytesIO(IGMP_PACKETS['v2-report'][0]), 8) # type: ignore[abstract] def test_ipv4_dispatches_igmp_to_each_version(self) -> None: - from pcapkit.protocols.internet.igmpv1 import IGMP as IGMPv1 - from pcapkit.protocols.internet.igmpv2 import IGMP as IGMPv2 - from pcapkit.protocols.internet.igmpv3 import IGMP as IGMPv3 + from pcapkit.protocols.internet.igmpv1 import IGMPv1 + from pcapkit.protocols.internet.igmpv2 import IGMPv2 + from pcapkit.protocols.internet.igmpv3 import IGMPv3 from pcapkit.protocols.internet.ipv4 import IPv4 expected_classes = {'IGMPv1': IGMPv1, 'IGMPv2': IGMPv2, 'IGMPv3': IGMPv3} From 00b11ade7a24c4dff307b98090593573d0aa85bb Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Sun, 1 Nov 2026 03:06:51 -0500 Subject: [PATCH 2/2] fix(internet): clear IGMP reachability gaps and per-version output keys (#2529) - Reachability audit: accept a class that inherits its family base's __index__ (new _inherits_index()), not a shared module or __name__; IGMPv1/2/3 now pass through IGMP's entry. Same hunk as the DRARP move. - igmp.rst: key the shared IGMPv1/v2 table as igmpv1.*, noting IGMPv2's keys start igmpv2. - igmpv3.rst and the igmpv3 module docstring: key both tables as igmpv3.* on the flat Query/Report data fields; split the flags octet into resv, s_flag and qrv; leave Number of Sources unkeyed, as it is not output. - protocol-family-grouping.rst: the name match ignores case and underscores; schema/ and data/ modules follow the same layout; list the classes still pending under #2529. --- .../conventions/protocol-family-grouping.rst | 13 ++++++++ .../pcapkit/protocols/internet/igmp.rst | 11 ++++--- .../pcapkit/protocols/internet/igmpv3.rst | 30 ++++++++++--------- pcapkit/protocols/internet/igmpv3.py | 30 ++++++++++--------- .../test_dispatch_reachability_unit.py | 24 ++++++++++----- 5 files changed, 68 insertions(+), 40 deletions(-) diff --git a/docs/source/contributing/conventions/protocol-family-grouping.rst b/docs/source/contributing/conventions/protocol-family-grouping.rst index 21e85f55f9..a3e634ac8f 100644 --- a/docs/source/contributing/conventions/protocol-family-grouping.rst +++ b/docs/source/contributing/conventions/protocol-family-grouping.rst @@ -186,3 +186,16 @@ class.** :mod:`~pcapkit.protocols.protocol` is exempt: its :class:`~pcapkit.protocols.protocol.ProtocolBase` and :class:`~pcapkit.protocols.protocol.Protocol` are framework bases rather than dissectors. Both ruled on :issue:`2529`. + +The name match ignores case and underscores: :mod:`~pcapkit.protocols.link.c_tag` holds +:class:`~pcapkit.protocols.link.c_tag.C_Tag` and :mod:`~pcapkit.protocols.link.linux_sll1` +holds :class:`~pcapkit.protocols.link.linux_sll1.LinuxSLL1`. **The** ``schema/`` **and** +``data/`` **modules follow the same layout**: the top-level class takes the module's +name, and sub-structure classes such as +:class:`~pcapkit.protocols.schema.internet.igmpv3.Query` share its module. Ruled on +:issue:`2529`, where the classes that do not match yet are pending: +:class:`httpv1.HTTP `, +:class:`httpv2.HTTP `, +:class:`arp.InARP `, +:class:`tls.SSLv3 ` and +:class:`null.NoPayload `. diff --git a/docs/source/pcapkit/protocols/internet/igmp.rst b/docs/source/pcapkit/protocols/internet/igmp.rst index dc83af0c21..876b476189 100644 --- a/docs/source/pcapkit/protocols/internet/igmp.rst +++ b/docs/source/pcapkit/protocols/internet/igmp.rst @@ -17,13 +17,16 @@ instead -- see :doc:`igmpv3`. message is read as: ``'IGMP'`` for IGMPv1, the name :rfc:`1112` gives it, and ``'IGMPv2'`` or ``'IGMPv3'`` for the others. +The structure is keyed as for IGMPv1; an IGMPv2 message's keys start ``igmpv2`` +instead: + ======= ========= ========================= =================================== Octets Bits Name Description ======= ========= ========================= =================================== - 0 0 ``igmp.type`` Type - 1 8 ``igmp.max_resp_time`` Max Response Time - 2 16 ``igmp.checksum`` Checksum - 4 32 ``igmp.group`` Group Address + 0 0 ``igmpv1.type`` Type + 1 8 ``igmpv1.max_resp_time`` Max Response Time + 2 16 ``igmpv1.checksum`` Checksum + 4 32 ``igmpv1.group`` Group Address ======= ========= ========================= =================================== .. autoclass:: pcapkit.protocols.internet.igmp.IGMP diff --git a/docs/source/pcapkit/protocols/internet/igmpv3.rst b/docs/source/pcapkit/protocols/internet/igmpv3.rst index 96cac1bb7a..27c78ae0e5 100644 --- a/docs/source/pcapkit/protocols/internet/igmpv3.rst +++ b/docs/source/pcapkit/protocols/internet/igmpv3.rst @@ -19,14 +19,16 @@ appends a source-list extension: ======= ========= ================================== =================================== Octets Bits Name Description ======= ========= ================================== =================================== - 0 0 ``igmp.type`` Type - 1 8 ``igmp.body.max_resp_code`` Max Response Code - 2 16 ``igmp.body.checksum`` Checksum - 4 32 ``igmp.body.group`` Group Address - 8 64 ``igmp.body.ext.flags`` Resv / S Flag / QRV - 9 72 ``igmp.body.ext.qqic`` Querier's Query Interval Code - 10 80 ``igmp.body.ext.num_sources`` Number of Sources (N) - 12 96 ``igmp.body.ext.sources`` Source Address [1..N] + 0 0 ``igmpv3.type`` Type + 1 8 ``igmpv3.max_resp_code`` Max Response Code + 2 16 ``igmpv3.checksum`` Checksum + 4 32 ``igmpv3.group`` Group Address + 8 64 ``igmpv3.resv`` Reserved (Resv) + 8 68 ``igmpv3.s_flag`` Suppress Router-Side Processing (S) + 8 69 ``igmpv3.qrv`` Querier's Robustness Variable (QRV) + 9 72 ``igmpv3.qqic`` Querier's Query Interval Code + 10 80 Number of Sources (N) + 12 96 ``igmpv3.sources`` Source Address [1..N] ======= ========= ================================== =================================== An IGMPv3 Membership Report (type ``0x22``) has an unrelated, materially @@ -35,12 +37,12 @@ different shape of its own: ======= ========= ================================== =================================== Octets Bits Name Description ======= ========= ================================== =================================== - 0 0 ``igmp.type`` Type - 1 8 ``igmp.body.reserved_1`` Reserved - 2 16 ``igmp.body.checksum`` Checksum - 4 32 ``igmp.body.reserved_2`` Reserved - 6 48 ``igmp.body.num_group_records`` Number of Group Records (M) - 8 64 ``igmp.body.records`` Group Record [1..M] + 0 0 ``igmpv3.type`` Type + 1 8 ``igmpv3.reserved_1`` Reserved + 2 16 ``igmpv3.checksum`` Checksum + 4 32 ``igmpv3.reserved_2`` Reserved + 6 48 ``igmpv3.num_group_records`` Number of Group Records (M) + 8 64 ``igmpv3.records`` Group Record [1..M] ======= ========= ================================== =================================== .. autoclass:: pcapkit.protocols.internet.igmpv3.IGMPv3 diff --git a/pcapkit/protocols/internet/igmpv3.py b/pcapkit/protocols/internet/igmpv3.py index 68f354c3ad..57a9aca5ac 100644 --- a/pcapkit/protocols/internet/igmpv3.py +++ b/pcapkit/protocols/internet/igmpv3.py @@ -17,14 +17,16 @@ ======= ========= ================================== =================================== Octets Bits Name Description ======= ========= ================================== =================================== - 0 0 ``igmp.type`` Type - 1 8 ``igmp.body.max_resp_code`` Max Response Code - 2 16 ``igmp.body.checksum`` Checksum - 4 32 ``igmp.body.group`` Group Address - 8 64 ``igmp.body.ext.flags`` Resv / S Flag / QRV - 9 72 ``igmp.body.ext.qqic`` Querier's Query Interval Code - 10 80 ``igmp.body.ext.num_sources`` Number of Sources (N) - 12 96 ``igmp.body.ext.sources`` Source Address [1..N] + 0 0 ``igmpv3.type`` Type + 1 8 ``igmpv3.max_resp_code`` Max Response Code + 2 16 ``igmpv3.checksum`` Checksum + 4 32 ``igmpv3.group`` Group Address + 8 64 ``igmpv3.resv`` Reserved (Resv) + 8 68 ``igmpv3.s_flag`` Suppress Router-Side Processing (S) + 8 69 ``igmpv3.qrv`` Querier's Robustness Variable (QRV) + 9 72 ``igmpv3.qqic`` Querier's Query Interval Code + 10 80 Number of Sources (N) + 12 96 ``igmpv3.sources`` Source Address [1..N] ======= ========= ================================== =================================== An IGMPv3 Membership Report (type ``0x22``) has an unrelated, materially @@ -33,12 +35,12 @@ ======= ========= ================================== =================================== Octets Bits Name Description ======= ========= ================================== =================================== - 0 0 ``igmp.type`` Type - 1 8 ``igmp.body.reserved_1`` Reserved - 2 16 ``igmp.body.checksum`` Checksum - 4 32 ``igmp.body.reserved_2`` Reserved - 6 48 ``igmp.body.num_group_records`` Number of Group Records (M) - 8 64 ``igmp.body.records`` Group Record [1..M] + 0 0 ``igmpv3.type`` Type + 1 8 ``igmpv3.reserved_1`` Reserved + 2 16 ``igmpv3.checksum`` Checksum + 4 32 ``igmpv3.reserved_2`` Reserved + 6 48 ``igmpv3.num_group_records`` Number of Group Records (M) + 8 64 ``igmpv3.records`` Group Record [1..M] ======= ========= ================================== =================================== :meth:`~pcapkit.protocols.internet.igmp.IGMP._classify` is what keeps an diff --git a/tests/protocols/test_dispatch_reachability_unit.py b/tests/protocols/test_dispatch_reachability_unit.py index b140c5cdb0..48d4b879a1 100644 --- a/tests/protocols/test_dispatch_reachability_unit.py +++ b/tests/protocols/test_dispatch_reachability_unit.py @@ -70,6 +70,16 @@ def _resolve(entry: 'Any') -> 'tuple[Any, Any]': return getattr(entry, '__module__', None), getattr(entry, '__name__', None) +def _inherits_index(cls: 'type', module: 'Any', name: 'Any') -> 'bool': + """Whether ``cls`` inherits its ``__index__`` from the class ``module.name``.""" + if module is None or name is None: + return False + target = getattr(importlib.import_module(module), name, None) + if not isinstance(target, type) or not issubclass(cls, target): + return False + return getattr(cls.__index__, '__func__', None) is getattr(target.__index__, '__func__', None) + + def _descendants(cls: 'type') -> 'Any': """Every subclass of ``cls``, transitively.""" for sub in cls.__subclasses__(): @@ -121,13 +131,11 @@ def audit() -> 'tuple[list[Gap], int]': # re-export rather than the defining module -- ``Frame.__proto__`` # holds ``pcapkit.protocols.link:Ethernet``, not # ``pcapkit.protocols.link.ethernet:Ethernet``; - # * the same *module*, because the project's rule is that a shared - # index may share a module -- ``InARP`` shares ``ARP``'s, and - # ``DRARP`` shares ``RARP``'s -- and a distinct one may not, per - # ``test_vlan_indices_follow_the_one_module_per_index_rule``. Both - # classes answer to the code and the module they share is what - # dispatches between them, so the sibling's name is not a mismatch. - if name == cls.__name__ or module == cls.__module__: + # * a class this one inherits its ``__index__`` from, because a + # member sharing its family base's index is parsed by the base and + # reported through its alias -- ``InARP`` by ``ARP``, ``DRARP`` by + # ``RARP`` (#2529). + if name == cls.__name__ or _inherits_index(cls, module, name): checked += 1 continue @@ -155,7 +163,7 @@ def test_every_indexed_class_is_reachable_under_the_code_it_declares(self) -> No # A walk that found nothing to check would also report no gaps, so pin # a floor of 23 claims (measured: the 21 distinct classes plus ``InARP`` - # and ``DRARP`` reaching their siblings' entries, and ``Ethernet`` + # and ``DRARP`` reaching their family bases' entries, and ``Ethernet`` # counted once for ``Frame`` and once for ``PCAPNG``). self.assertGreaterEqual(checked, 23)