Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ Preceded by `1.5.0a1` (2026-09-15), `1.5.0b1` to `1.5.0b3` (all 2026-09-19) and
- the Mobility Header registry, completed ([#383](https://github.com/JarryShaw/PyPCAPKit/pull/383), [#437](https://github.com/JarryShaw/PyPCAPKit/pull/437)): the [RFC 5568](https://datatracker.ietf.org/doc/html/rfc5568) fast-handover messages and options, then every registered message data type, every registered option (with nested sub-option registries for the flow identification, access network identifier, quality-of-service and LMA-controlled MAG parameter families) and every CGA extension. The CGA Parameters option, the only option that can carry a CGA extension on the wire, was the last to come off the generic handler, which is what let the CGA extensions round-trip end to end.
- dispatch entries for dissectors that existed but were reachable from no registry ([#436](https://github.com/JarryShaw/PyPCAPKit/pull/436)): FTP-DATA on TCP 20, HTTP/1 on TCP 8080, HTTP on UDP 8080, `L2TPv2` on UDP 1701 and OSPF at `TransType` 89. `VLAN` became an abstract base with `C_Tag` (802.1Q) and `S_Tag` (802.1ad) as concrete subclasses, so a Q-in-Q frame no longer collapses into one opaque `Raw`; `L2TP` likewise became a base, with `L2TPv2` carrying the [RFC 2661](https://datatracker.ietf.org/doc/html/rfc2661) implementation.
- `Protocol`/`ProtocolBase` gain a `code=` class keyword for the next-layer dispatch registries (`Link.__proto__`, `Internet.__proto__`, `TCP.__proto__`, `UDP.__proto__`, `SCTP.__proto__`, `Frame.__proto__`, `PCAPNG.__proto__`), the opt-in treatment [#514](https://github.com/JarryShaw/PyPCAPKit/issues/514) gave `Engine`, `Reassembly`, `TraceFlow` and `Dumper` above, extended to the one family that needed a registration key invented. Omitting `code` leaves a subclass unregistered, as before; the built-in tables are still filled by literal assignment in each layer module, so nothing the library ships moves. `code` accepts a bare enum member, whose *type* infers the destination (`EtherType` means `Link`, `TransType` means `Internet`, `PayloadProtocolIdentifier` means `SCTP`, and `LinkType` means *both* `Frame` **and** `PCAPNG`, mirroring `register_linktype`), or a `{destination: key}` mapping, required for a raw `int` such as a port number, which cannot say which transport it belongs to. Either form may appear in an iterable, so one declaration can register a class into several registries (e.g. a `L2TP` subclass reachable by IP protocol number and by a UDP port), and the mapping form is accepted even where inference would work. Inference refuses rather than guesses: an enum member whose type names no known destination raises `RegistryError`, and an unrecognised class keyword raises `UnsupportedCall`, matching the other registrar families. Backed by `pcapkit.foundation.registry.protocols.register_protocol_code()`, also callable directly for a class that declined at definition time. It was written up as the mechanism [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548) needs; [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548) turned out to need a missing class instead (115 is [RFC 3931](https://datatracker.ietf.org/doc/html/rfc3931) L2TPv3 over IP, not [RFC 2661](https://datatracker.ietf.org/doc/html/rfc2661); see the corresponding **Fixed** entry below), so its worked example names `L2TPv3` ([#570](https://github.com/JarryShaw/PyPCAPKit/pull/570)).
- `tests/protocols/test_dispatch_reachability_unit.py`, the coverage [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548) asked for: every `ProtocolBase` descendant whose `__index__` returns an enum member must be reachable under that code in the registry its enum *type* designates, read from the same `_CODE_DESTINATIONS` table backing `code=` so the two cannot drift. It walks the classes rather than the registry entries, so it catches a class nothing registered, the shape in which `OSPF` once shipped ([#548](https://github.com/JarryShaw/PyPCAPKit/issues/548)).
- `tests/protocols/test_dispatch_reachability_unit.py`, the coverage [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548) asked for: every `ProtocolBase` descendant whose `__index__` returns an enum member must be reachable under that code in the registry its enum *type* designates, read from the same `__proto_dest__` declaration backing `code=` so the two cannot drift. It walks the classes rather than the registry entries, so it catches a class nothing registered, the shape in which `OSPF` once shipped ([#548](https://github.com/JarryShaw/PyPCAPKit/issues/548)).
- `tests/protocols/transport/test_tcp_mptcp_join_flag_ordering_unit.py`, covering all three MP_JOIN layouts through the *public* constructor, the construct-pack-parse cycle for each, the stale-flags case that rules out a zero-valued default, the statement order, and controls for the parse path and the flag-independent options. The old cases bypassed the public path, so full coverage coexisted with a broken one. The stale `tcp-mptcp/MP_JOIN` entry is deleted from `EXPECTED_FAILURES` ([#587](https://github.com/JarryShaw/PyPCAPKit/issues/587)).
- `tests/protocols/test_option_generator_tcp_base_unit.py`, asserting that every `TCP_BASE` key is a parameter `TCP.make()` declares (derived from `inspect.signature`) and that the built segment carries the stated header in both data model and packed octets ([#602](https://github.com/JarryShaw/PyPCAPKit/issues/602)).
- ILNP nonce sizing coverage in `tests/protocols/internet/test_ipv6_extension_unit.py`, a test per protocol, asserting the declared length, the exact packed octets and the construct-pack-parse cycle over a spread of nonces, most with a bit length that is *not* a multiple of eight, where floor division and the ceiling disagree ([#601](https://github.com/JarryShaw/PyPCAPKit/issues/601)).
Expand Down
2 changes: 1 addition & 1 deletion docs/source/changelog/1.5.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1303,7 +1303,7 @@ Added
* ``tests/protocols/test_dispatch_reachability_unit.py``, the coverage :issue:`548`
asked for: every :class:`.ProtocolBase` descendant whose ``__index__`` returns an enum
member must be reachable under that code in the registry its enum *type*
designates, read from the same ``_CODE_DESTINATIONS`` table backing ``code=`` so
designates, read from the same ``__proto_dest__`` declaration backing ``code=`` so
the two cannot drift. It walks the classes rather than the registry entries, so it
catches a class nothing registered, the shape in which
:class:`~pcapkit.protocols.application.ospf.OSPF` once shipped (:issue:`548`).
Expand Down
3 changes: 3 additions & 0 deletions docs/source/pcapkit/const/reg.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ which is automatically generated from :class:`pcapkit.vendor.reg.linktype.LinkTy
:members:
:undoc-members:
:show-inheritance:
:special-members: __proto_dest__

Ethertype IEEE 802 Numbers
==========================
Expand All @@ -43,6 +44,7 @@ which is automatically generated from :class:`pcapkit.vendor.reg.ethertype.Ether
:members:
:undoc-members:
:show-inheritance:
:special-members: __proto_dest__

Transport Layer Protocol Numbers
================================
Expand All @@ -56,6 +58,7 @@ which is automatically generated from :class:`pcapkit.vendor.reg.transtype.Trans
:members:
:undoc-members:
:show-inheritance:
:special-members: __proto_dest__

Application Layer Protocol Numbers
==================================
Expand Down
1 change: 1 addition & 0 deletions docs/source/pcapkit/const/sctp.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ which is automatically generated from :class:`pcapkit.vendor.sctp.payload_protoc
:members:
:undoc-members:
:show-inheritance:
:special-members: __proto_dest__

.. rubric:: Footnotes

Expand Down
11 changes: 10 additions & 1 deletion docs/source/pcapkit/vendor/default.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ only, which is the base class for all vendor crawlers.
:no-value:
.. autoattribute:: LINK
:no-value:
.. autoattribute:: PROTO_DEST

.. automethod:: count
.. automethod:: process
Expand Down Expand Up @@ -76,7 +77,7 @@ Internal Definitions
:no-members:
:show-inheritance:

.. function:: LINE(NAME, DOCS, FLAG, ENUM, MISS, MODL)
.. function:: LINE(NAME, DOCS, FLAG, ENUM, MISS, MODL, DEST=())

Default constant template of enumeration registry from IANA CSV.

Expand All @@ -86,8 +87,16 @@ Internal Definitions
:param str ENUM: enumeration data (class attributes)
:param str MISS: missing value handler (default value)
:param str MODL: module name of the constant enumeration class
:param DEST: the crawler's :attr:`~pcapkit.vendor.default.Vendor.PROTO_DEST`,
rendered as the class's ``__proto_dest__``; nothing is rendered when it is
empty, which is the case for every registry that keys no ``__proto__``
:type DEST: tuple[~pcapkit.corekit.module.ModuleDescriptor, ...]
:rtype: str

.. autodata:: pcapkit.vendor.default.PROTO_DEST_IMPORT

.. autofunction:: pcapkit.vendor.default.render_proto_dest

.. autofunction:: pcapkit.vendor.default.get_proxies

.. autofunction:: pcapkit.vendor.default.stdin_is_interactive
12 changes: 12 additions & 0 deletions pcapkit/const/reg/ethertype.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from aenum import IntEnum

from pcapkit.corekit.enum import EnumRegistry
from pcapkit.corekit.module import ModuleDescriptor
from pcapkit.utilities.exceptions import EnumValueError

if TYPE_CHECKING:
Expand All @@ -26,6 +27,17 @@
class EtherType(EnumRegistry, IntEnum):
"""[EtherType] Ethertype IEEE 802 Numbers"""

#: Protocol class(es) owning the ``__proto__`` dispatch registry keyed by
#: this enumeration, read by
#: :func:`~pcapkit.foundation.registry.protocols.register_protocol_code`
#: to resolve a bare member of it into its destination(s). The classes are
#: named as lazy :class:`~pcapkit.corekit.module.ModuleDescriptor`
#: references rather than imported, because :mod:`pcapkit.const` sits below
#: :mod:`pcapkit.protocols` and importing one from here would be circular.
__proto_dest__ = (
ModuleDescriptor('pcapkit.protocols.link.link', 'Link'),
)

#: Formerly XEROX PUP. Invalid as an Ethertype since 1983. Use 0x0A00. [Boggs,
#: D., J. Shoch, E. Taft, and R. Metcalfe, "PUP: An Internetwork Architecture",
#: XEROX Palo Alto Research Center, CSL-79-10, July 1979; also in IEEE
Expand Down
13 changes: 13 additions & 0 deletions pcapkit/const/reg/linktype.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from aenum import IntEnum

from pcapkit.corekit.enum import EnumRegistry
from pcapkit.corekit.module import ModuleDescriptor
from pcapkit.utilities.exceptions import EnumValueError

if TYPE_CHECKING:
Expand All @@ -26,6 +27,18 @@
class LinkType(EnumRegistry, IntEnum):
"""[LinkType] Link-Layer Header Type Values"""

#: Protocol class(es) owning the ``__proto__`` dispatch registry keyed by
#: this enumeration, read by
#: :func:`~pcapkit.foundation.registry.protocols.register_protocol_code`
#: to resolve a bare member of it into its destination(s). The classes are
#: named as lazy :class:`~pcapkit.corekit.module.ModuleDescriptor`
#: references rather than imported, because :mod:`pcapkit.const` sits below
#: :mod:`pcapkit.protocols` and importing one from here would be circular.
__proto_dest__ = (
ModuleDescriptor('pcapkit.protocols.misc.pcap.frame', 'Frame'),
ModuleDescriptor('pcapkit.protocols.misc.pcapng', 'PCAPNG'),
)

#: [``DLT_NULL``] BSD loopback encapsulation.
NULL = 0

Expand Down
12 changes: 12 additions & 0 deletions pcapkit/const/reg/transtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from aenum import IntEnum

from pcapkit.corekit.enum import EnumRegistry
from pcapkit.corekit.module import ModuleDescriptor
from pcapkit.utilities.exceptions import EnumValueError

if TYPE_CHECKING:
Expand All @@ -26,6 +27,17 @@
class TransType(EnumRegistry, IntEnum):
"""[TransType] Transport Layer Protocol Numbers"""

#: Protocol class(es) owning the ``__proto__`` dispatch registry keyed by
#: this enumeration, read by
#: :func:`~pcapkit.foundation.registry.protocols.register_protocol_code`
#: to resolve a bare member of it into its destination(s). The classes are
#: named as lazy :class:`~pcapkit.corekit.module.ModuleDescriptor`
#: references rather than imported, because :mod:`pcapkit.const` sits below
#: :mod:`pcapkit.protocols` and importing one from here would be circular.
__proto_dest__ = (
ModuleDescriptor('pcapkit.protocols.internet.internet', 'Internet'),
)

#: IPv6 Hop-by-Hop Option [:rfc:`8200`]
HOPOPT = 0

Expand Down
12 changes: 12 additions & 0 deletions pcapkit/const/sctp/payload_protocol_identifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from aenum import IntEnum

from pcapkit.corekit.enum import EnumRegistry
from pcapkit.corekit.module import ModuleDescriptor
from pcapkit.utilities.exceptions import EnumValueError

if TYPE_CHECKING:
Expand All @@ -26,6 +27,17 @@
class PayloadProtocolIdentifier(EnumRegistry, IntEnum):
"""[PayloadProtocolIdentifier] SCTP Payload Protocol Identifiers"""

#: Protocol class(es) owning the ``__proto__`` dispatch registry keyed by
#: this enumeration, read by
#: :func:`~pcapkit.foundation.registry.protocols.register_protocol_code`
#: to resolve a bare member of it into its destination(s). The classes are
#: named as lazy :class:`~pcapkit.corekit.module.ModuleDescriptor`
#: references rather than imported, because :mod:`pcapkit.const` sits below
#: :mod:`pcapkit.protocols` and importing one from here would be circular.
__proto_dest__ = (
ModuleDescriptor('pcapkit.protocols.transport.sctp', 'SCTP'),
)

#: Reserved by SCTP [:rfc:`9260`]
Reserved_by_SCTP = 0

Expand Down
52 changes: 25 additions & 27 deletions pcapkit/foundation/registry/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@
from pcapkit.const.reg.transtype import TransType
from pcapkit.const.sctp.payload_protocol_identifier import \
PayloadProtocolIdentifier as SCTP_PayloadProtocolIdentifier
from pcapkit.corekit.enum import EnumRegistry
from pcapkit.corekit.sentinels import NullType
from pcapkit.protocols.application.httpv2 import FrameConstructor as HTTP_FrameConstructor
from pcapkit.protocols.application.httpv2 import FrameParser as HTTP_FrameParser
Expand Down Expand Up @@ -405,25 +404,6 @@ def _register_name(protocol: 'Type[ProtocolBase]', *targets: 'tuple[type, Any]')
_CODE_LEASES[target] = lease


#: Destination classes for each enum type a ``code=`` key can carry.
#:
#: Maps an enum type to the class(es) owning the
#: :attr:`ProtocolBase.__proto__ <pcapkit.protocols.protocol.ProtocolBase.__proto__>`
#: dispatch registry keyed by that enum type -- the "registry-of-registries" that
#: lets ``code=`` infer a destination from a key's own type, per :issue:`514`. It
#: is the same targeting :func:`register_ethertype`, :func:`register_transtype`,
#: :func:`register_linktype` and :func:`register_sctp` hard-code by hand, in one
#: table so :func:`register_protocol_code` can consult it. :class:`~pcapkit.const.reg.linktype.LinkType` naming
#: more than one class is deliberate, not ambiguous -- :func:`register_linktype`
#: already fans out to each of them.
_CODE_DESTINATIONS: 'dict[Type[EnumRegistry], tuple[Type[ProtocolBase], ...]]' = {
Enum_EtherType: (Link,),
Enum_TransType: (Internet,),
Enum_PayloadProtocolIdentifier: (SCTP,),
Enum_LinkType: (Frame, PCAPNG),
}


def _iter_code_targets(code: 'Any') -> 'Iterator[tuple[Type[ProtocolBase], Any]]':
"""Flatten a ``code=`` argument into ``(destination, key)`` pairs.

Expand All @@ -445,13 +425,29 @@ def _iter_code_targets(code: 'Any') -> 'Iterator[tuple[Type[ProtocolBase], Any]]
return

if isinstance(code, (enum.Enum, aenum.Enum)):
destinations = _CODE_DESTINATIONS.get(cast('Type[EnumRegistry]', type(code)))
if destinations is None:
# NOTE: read with ``getattr``, not out of the type's own ``vars()`` as
# the ``__proto_key__`` check in ``register_protocol_code`` does. That
# read is deliberately non-inheriting because a protocol subclass can
# own a registry keyed by something other than its parent's -- Loopback
# does. No enum registry can be in that position: declaring a
# destination means having members, and aenum refuses to extend an
# enumeration with members, so none of these registries has a subclass
# and none can gain one. The one enum registry that is subclassed is
# AppType, a memberless base partitioned by transport protocol, and its
# subclass relationship is irrelevant here only because AppType itself
# declares no ``__proto_dest__`` at all -- the inheriting read finds
# nothing to inherit, so none of its parts reaches this branch. A
# class-level destination would in fact be the wrong model for it:
# ``register_apptype`` routes each member to the registry named by that
# member's own transport protocol, which one shared destination for the
# whole family could not express.
destinations = getattr(type(code), '__proto_dest__', None)
if not destinations:
raise RegistryError(
f'no destination registry is known for enum type {type(code).__name__!r} '
f'(key {code!r}); pass an explicit destination, e.g. code={{SomeClass: {code!r}}}')
for destination in destinations:
yield destination, code
for descriptor in destinations:
yield descriptor.klass, code
return

if isinstance(code, (str, bytes)):
Expand Down Expand Up @@ -479,8 +475,9 @@ def register_protocol_code(protocol: 'Type[ProtocolBase]', code: 'Any') -> 'None
``code`` is normalised into a flat sequence of targets, where each item
is either:

* an enum member, whose *type* is looked up in a small table mapping
enum type to the class(es) owning the matching ``__proto__`` --
* an enum member, whose *type* names the class(es) owning the matching
``__proto__`` in its
:attr:`__proto_dest__ <pcapkit.const.reg.ethertype.EtherType.__proto_dest__>` --
:class:`~pcapkit.const.reg.ethertype.EtherType` to
:class:`~pcapkit.protocols.link.link.Link`,
:class:`~pcapkit.const.reg.transtype.TransType` to
Expand Down Expand Up @@ -527,7 +524,8 @@ def register_protocol_code(protocol: 'Type[ProtocolBase]', code: 'Any') -> 'None
Raises:
RegistryError: If a bare :class:`int` (or any other non-enum,
non-mapping value) is given without an explicit destination, or if
an enum member's type names no known destination registry --
an enum member's type declares no
:attr:`__proto_dest__ <pcapkit.const.reg.ethertype.EtherType.__proto_dest__>` --
inference refuses rather than guesses; or if a key is not of the
type its destination's registry is keyed by, as its typed registrar
would reject it, e.g. ``{Frame: TransType.ICMP}``. What a registry is
Expand Down
Loading
Loading