# -*- coding: utf-8 -*-
"""Protocol Base Registries
==============================
.. module:: pcapkit.foundation.registry.protocols.protocol
This module provides the protocol registries the per-layer modules beside it
build on: :func:`register_protocol` for the by-name registry
:attr:`ProtocolBase.__registry__ <pcapkit.protocols.protocol.ProtocolBase.__registry__>`,
:func:`register_protocol_code` for the
next-layer registries a ``code`` names, and the validation and by-name
bookkeeping every registrar shares.
.. versionadded:: 1.6.0
Moved here from the single :file:`pcapkit/foundation/registry/protocols.py`
module. The :mod:`pcapkit.foundation.registry.protocols` package exports
this module's public functions (:issue:`2578`).
"""
import collections.abc
import enum
import sys
import threading
from typing import TYPE_CHECKING, cast
import aenum
from pcapkit.corekit.module import ModuleDescriptor
from pcapkit.corekit.sentinels import MISSING
from pcapkit.protocols.protocol import ProtocolBase
from pcapkit.utilities.exceptions import RegistryError
from pcapkit.utilities.logging import get_logger
if TYPE_CHECKING:
from typing import Any, Iterator, Optional, Type
__all__ = [
'register_protocol',
'register_protocol_code',
]
#: logging.Logger: Module-level logger, a child of the package-wide
#: :data:`pcapkit.utilities.logging.logger`.
logger = get_logger(__name__)
# NOTE: pcapkit.protocols.protocol.ProtocolBase.__registry__
[docs]
def register_protocol(protocol: 'Type[ProtocolBase]') -> 'None':
"""Register a protocol class.
The protocol class must be a subclass of
:class:`~pcapkit.protocols.protocol.ProtocolBase`, and will be registered to
the by-name registry :attr:`ProtocolBase.__registry__
<pcapkit.protocols.protocol.ProtocolBase.__registry__>` through
:meth:`ProtocolBase.register_protocol
<pcapkit.protocols.protocol.ProtocolBase.register_protocol>`, replacing the
entry its name holds.
The registry is keyed on ``protocol.__name__.upper()``, which nothing makes
unique: two protocol classes of the same name, such as a user's class
named after a shipped one, compete for one key. Defining the second as a
:class:`~pcapkit.protocols.protocol.Protocol` subclass keeps the first, and
registering it here replaces whichever was there, and the
replacement is observable through every reader of the registry, e.g.
:meth:`Protocol.expand_comp
<pcapkit.protocols.protocol.Protocol.expand_comp>`, which resolves a
bare protocol name through it. Per :issue:`675` the overwrite is reported,
matching :meth:`Protocol.register
<pcapkit.protocols.protocol.Protocol.register>` and the other
overwrite-warning registries.
The guard reads "key present **and** incumbent is a different class" --
presence alone is not enough. This registry's key is *derived* from the
class rather than supplied by a caller, and this function is the funnel
every wrapper registrar calls --
:func:`~pcapkit.foundation.registry.protocols.transport.register_tcp`,
:func:`~pcapkit.foundation.registry.protocols.transport.register_udp`,
:func:`~pcapkit.foundation.registry.protocols.transport.register_apptype`,
:func:`~pcapkit.foundation.registry.protocols.misc.register_linktype` and
the rest all end by calling it with their class, and registering their
code back undoes that call's write (:issue:`1505`). So registering one class under two codes, a
supported and documented thing to do, reaches this function twice with the
same class and nothing at stake; a presence-only guard would warn about an
overwrite that overwrote nothing. Warning on the harmless case is not free:
it teaches a caller to filter
:exc:`~pcapkit.utilities.warnings.RegistryWarning` wholesale, and that
filter would then hide the name collision this warning exists to
surface. The sibling ``register`` methods across the package, each keyed on a
caller-supplied ``code``, apply the same identity criterion (:issue:`718`).
A built-in's :class:`~pcapkit.corekit.module.ModuleDescriptor` entry is
the class it names for that criterion.
Making the key itself unique would resolve the collision rather than
merely reporting it, but it is a registry-format change that the bare-name
readers outside this module cannot absorb on their own -- the registry is a
documented public attribute, and its readers look a bare name up and then
degrade *silently* on a miss rather than raising, so a re-keying would not
announce itself either. :meth:`Protocol.expand_comp
<pcapkit.protocols.protocol.Protocol.expand_comp>` falls back to the
name as a plain string, and the ``protocol`` properties on
:class:`~pcapkit.foundation.reassembly.reassembly.ReassemblyMeta` and
:class:`~pcapkit.foundation.traceflow.traceflow.TraceFlowMeta` fall back to
:class:`~pcapkit.protocols.misc.raw.protocol.Raw`. The registry redesign of
:issue:`514` keeps this key, and its thread records the shared namespace as
a risk that design does not resolve, so the collision is reported here
rather than fixed.
Args:
protocol: Protocol class.
Raises:
pcapkit.utilities.exceptions.RegistryError: If ``protocol`` is not a
:class:`~pcapkit.protocols.protocol.ProtocolBase` subclass.
Warns:
pcapkit.utilities.warnings.RegistryWarning: If the registry already
holds a *different* class under this protocol's name, i.e. the
registration displaced another protocol class. Re-registering the
same class under the same name is silent.
.. versionchanged:: 1.6.0
The HTTP member classes are named
:class:`~pcapkit.protocols.application.httpv1.protocol.HTTPv1` and
:class:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2`, so registering
one keeps its own key and no longer displaces
:class:`~pcapkit.protocols.application.http.protocol.HTTP` under ``'HTTP'``
(:issue:`2529`). The registry is
:attr:`ProtocolBase.__registry__ <pcapkit.protocols.protocol.ProtocolBase.__registry__>`,
written by :meth:`ProtocolBase.register_protocol
<pcapkit.protocols.protocol.ProtocolBase.register_protocol>`, where it was
``pcapkit.protocols.__proto__`` (:issue:`2763`).
"""
if not isinstance(protocol, type):
raise RegistryError(f'protocol must be a class, not {protocol!r}')
if not issubclass(protocol, ProtocolBase):
raise RegistryError(f'protocol must be a Protocol subclass, not {protocol!r}')
ProtocolBase.register_protocol(protocol)
logger.debug('registered protocol: %s', protocol.__name__)
def _denotes(entry: 'Any', protocol: 'Any') -> 'bool':
"""Whether a by-name registry ``entry`` is ``protocol``, or a descriptor naming it.
A :class:`~pcapkit.corekit.module.ModuleDescriptor` names the class its
module binds under its name, read off :data:`sys.modules` rather than
imported: a class that is defined has its module loaded already.
Args:
entry: Registry entry, a displaced one included.
protocol: Protocol class.
"""
if entry is protocol:
return True
if isinstance(entry, ModuleDescriptor):
module = sys.modules.get(entry.module)
return module is not None and getattr(module, cast('str', entry.name), None) is protocol
return False
class _NameLease:
"""A by-name registry entry a code registration put there.
Each code registrar below ends by naming its class in the by-name registry,
and :mod:`pcapkit.foundation.registry` has no ``unregister``: a code is
undone by registering the entry it displaced (:issue:`1363`). Without a
record of that by-name write, handing a code back would restore the code and
leave the name behind (:issue:`1505`). A lease records what the write
displaced and which codes still dispatch to the class, so the last of them
to be handed back puts the displaced entry back, or removes the name if it
was new.
A lease is one write, not one class: the same class can hold two leases on
a name -- e.g. a built-in registered at one port over a custom class
shadowing it at another -- and each undoes only its own write.
"""
__slots__ = ('name', 'protocol', 'displaced', 'codes')
def __init__(self, name: 'str', protocol: 'Type[ProtocolBase]', displaced: 'Any') -> 'None':
#: The key written in the by-name registry.
self.name = name
#: The class written under it.
self.protocol = protocol
#: The entry it displaced, or :data:`~pcapkit.corekit.sentinels.MISSING` for a new name.
self.displaced = displaced
#: ``(destination, code)`` registrations still holding the lease.
self.codes = set() # type: set[tuple[type, Any]]
#: Name -> its leases, oldest first. Each lease displaced the one below it,
#: unless the name was written by other means in between; the last is the one
#: a lookup sees, unless the name has been written since.
_NAME_LEASES = {} # type: dict[str, list[_NameLease]]
#: ``(destination, code)`` -> the lease its registration holds.
_CODE_LEASES = {} # type: dict[tuple[type, Any], _NameLease]
#: Serialises the lease tables above, so concurrent registrars cannot tear them.
_LEASE_LOCK = threading.RLock()
def _fold_lease(stack: 'list[_NameLease]', index: 'int') -> 'None':
"""Retire ``stack[index]``, a lease now displacing its own class.
That happens once the lease between it and an earlier write of the same
class is handed back, e.g. a custom ``IPv4`` released from under the
built-in ``IPv4`` registered over it. Undoing it would change nothing, so
its codes move to the lease below when that is the same class, and
otherwise -- the class was named by other means -- hold nothing.
Args:
stack: The name's leases, oldest first.
index: Position of the lease to retire.
"""
lease = stack.pop(index)
below = stack[index - 1] if index > 0 else None
if below is not None and below.protocol is not lease.protocol:
below = None
for target in lease.codes:
if _CODE_LEASES.get(target) is not lease:
continue
if below is None:
del _CODE_LEASES[target]
else:
below.codes.add(target)
_CODE_LEASES[target] = below
def _release_name(target: 'tuple[type, Any]') -> 'None':
"""Hand back the lease ``target``'s registration held.
Once no code holds the lease, it leaves its name's stack. A lease on top
undoes its write, unless the name was written by other means since. A new
name is removed. A displaced class is registered back with
:func:`register_protocol`, the path a code's restore takes, so it warns as
the code's restore does (:issue:`2773`). A buried lease hands what it
displaced to the lease above, if that lease displaced it.
Args:
target: The ``(destination, code)`` being registered over.
"""
protocol_registry = ProtocolBase.__registry__
lease = _CODE_LEASES.pop(target, None)
if lease is None:
return
lease.codes.discard(target)
if lease.codes:
return
stack = _NAME_LEASES.get(lease.name, [])
index = next((i for i, item in enumerate(stack) if item is lease), None)
if index is None:
return
del stack[index]
if index < len(stack):
above = stack[index]
if _denotes(above.displaced, lease.protocol):
above.displaced = lease.displaced
if _denotes(above.displaced, above.protocol):
_fold_lease(stack, index)
elif _denotes(protocol_registry.get(lease.name), lease.protocol):
if lease.displaced is MISSING:
protocol_registry.pop(lease.name, None)
elif isinstance(lease.displaced, ModuleDescriptor):
register_protocol(lease.displaced.klass)
else:
register_protocol(lease.displaced)
logger.debug('released protocol: %s', lease.protocol.__name__)
if not stack:
_NAME_LEASES.pop(lease.name, None)
def _register_name(protocol: 'Type[ProtocolBase]', *targets: 'tuple[type, Any]') -> 'None':
"""Name ``protocol`` in the by-name registry for ``targets``.
The tail of every code registrar: :func:`register_protocol`, preceded by
releasing whatever each target code held before, and followed by a lease
when the write changed the name, so that registering the code back undoes
it (:issue:`1505`). A name already showing ``protocol`` -- shipped, named
when its concrete :class:`~pcapkit.protocols.protocol.Protocol` subclass was
defined, or registered directly -- is not
leased, so no restore takes it away; one showing it through a lease
extends that lease to ``targets``.
Args:
protocol: Protocol class just registered -- and so validated -- at ``targets``.
*targets: The ``(destination, code)`` pairs it was registered at.
"""
protocol_registry = ProtocolBase.__registry__
name = protocol.__name__.upper()
with _LEASE_LOCK:
stack = _NAME_LEASES.get(name)
if stack and stack[-1].protocol is protocol and _denotes(protocol_registry.get(name), protocol):
current = stack[-1] # type: Optional[_NameLease]
else:
current = None
for target in targets:
if current is None or _CODE_LEASES.get(target) is not current:
_release_name(target)
incumbent = protocol_registry.get(name, MISSING)
register_protocol(protocol)
if not _denotes(protocol_registry.get(name), protocol):
return
stack = _NAME_LEASES.get(name)
if not _denotes(incumbent, protocol):
lease = _NameLease(name, protocol, incumbent)
_NAME_LEASES.setdefault(name, []).append(lease)
elif stack and stack[-1].protocol is protocol:
lease = stack[-1]
else:
return
lease.codes.update(targets)
for target in targets:
_CODE_LEASES[target] = lease
def _iter_code_targets(code: 'Any') -> 'Iterator[tuple[Type[ProtocolBase], Any]]':
"""Flatten a ``code=`` argument into ``(destination, key)`` pairs.
Args:
code: See :func:`register_protocol_code`.
Yields:
One ``(destination, key)`` pair per registration the caller asked
for -- possibly several, e.g. a single :class:`~pcapkit.const.reg.\
linktype.LinkType` member yields both :class:`Frame` and :class:`PCAPNG`.
Raises:
RegistryError: If a value has no destination it can name or infer;
see :func:`register_protocol_code`.
"""
if isinstance(code, dict):
yield from code.items()
return
if isinstance(code, (enum.Enum, aenum.Enum)):
# NOTE: ``getattr`` may inherit: aenum refuses to extend an enumeration with members, so
# no registry has a subclass, and the memberless ``AppType`` declares no ``__proto_dest__``.
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 descriptor in destinations:
yield descriptor.klass, code
return
if isinstance(code, (str, bytes)):
raise RegistryError(f'code must be an enum member, a {{destination: key}} mapping, or an '
f'iterable thereof, not {code!r}')
if isinstance(code, collections.abc.Iterable):
for item in code:
yield from _iter_code_targets(item)
return
raise RegistryError(f'raw key {code!r} has no destination registry it can infer; pass an '
f'explicit destination, e.g. code={{TCP: {code!r}}}')
[docs]
def register_protocol_code(protocol: 'Type[ProtocolBase]', code: 'Any') -> 'None':
r"""Register ``protocol`` into the next-layer dispatch registry (or
registries) named by ``code``.
This is what backs the ``code`` class keyword of
:class:`~pcapkit.protocols.protocol.ProtocolBase` subclasses; it can also
be called directly to register a class that declined at class-definition
time.
``code`` is normalised into a flat sequence of targets, where each item
is either:
* 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
:class:`~pcapkit.protocols.internet.internet.Internet`,
:class:`~pcapkit.const.sctp.payload_protocol_identifier.PayloadProtocolIdentifier`
to :class:`~pcapkit.protocols.transport.sctp.protocol.SCTP`,
:class:`~pcapkit.const.linux.arphrd.ARPHRD` to
:class:`~pcapkit.protocols.link.linux_sll.protocol.LinuxSLL`, and
:class:`~pcapkit.const.reg.linktype.LinkType` to *both*
:class:`~pcapkit.protocols.misc.pcap.frame.protocol.Frame` **and**
:class:`~pcapkit.protocols.misc.pcapng.protocol.PCAPNG`; or
* a :class:`dict` mapping an explicit destination class to a key -- the
only form accepted for a bare :class:`int`, since e.g. a port number
does not say by itself whether it means
:class:`~pcapkit.protocols.transport.tcp.protocol.TCP` or
:class:`~pcapkit.protocols.transport.udp.protocol.UDP`;
or an iterable of either, to register ``protocol`` into several
registries from one call -- e.g. a
:class:`~pcapkit.protocols.link.l2tp.protocol.L2TP` subclass reachable both by its
IP protocol number and by a UDP port:
.. code-block:: python
register_protocol_code(L2TPv3, [TransType.L2TP, {UDP: 1701}])
The explicit mapping form is accepted for any key, even one whose type
could be inferred -- being more explicit than required is never an
error.
Note:
That example names ``L2TPv3``, which this package does not implement,
rather than :class:`~pcapkit.protocols.link.l2tpv2.protocol.L2TPv2`. It is
v3 that is genuinely reachable both ways: :rfc:`3931` §4.1.1 puts it
directly over IP on protocol 115 and §4.1.2 puts it over UDP on port
1701. :class:`L2TPv2 <pcapkit.protocols.link.l2tpv2.protocol.L2TPv2>` answers on
port 1701 only, and registering *it* at
:attr:`TransType.L2TP <pcapkit.const.reg.transtype.TransType.L2TP>` would point the
:rfc:`2661` parser at a v3-over-IP header -- see :class:`~pcapkit.protocols.link.l2tp.protocol.L2TP`
and GitHub issue :issue:`548` for what that produced when measured.
Args:
protocol: Protocol class to register.
code: Registration key(s); see above.
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 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
keyed by is read from the
:attr:`__proto_key__ <pcapkit.protocols.link.link.Link.__proto_key__>`
declared by the class owning it, so a destination sharing another's
registry is checked as its owner is, and one whose owner declares no
pairing is not checked. Every target is checked before any is
registered, so a rejected ``code`` leaves every registry as it was
(:issue:`2268`).
"""
targets = list(_iter_code_targets(code))
for destination, key in targets:
owner = next((klass for klass in getattr(destination, '__mro__', ())
if '__proto__' in vars(klass)), None)
if owner is None:
continue
# NOTE: read from the owner's own ``vars()``, not ``getattr``, so a class keeping its own
# registry, as ``Loopback`` does by address family, is not checked against its parent's.
pairing = vars(owner).get('__proto_key__')
if pairing is not None:
kind, number = pairing
_check_code(key, kind, owner, number=number)
for destination, key in targets:
destination.register(key, protocol)
logger.debug('registered %s into %s.__proto__: %s', protocol.__name__,
destination.__name__, key)
def _check_code(code: 'Any', kind: 'Optional[type]', target: 'Type[ProtocolBase]', *,
number: 'bool' = False) -> 'None':
"""Reject a registration key that is not a member of the enum its
destination registry is keyed by.
Each registrar typed on one enum calls this before touching any registry,
as does :func:`register_protocol_code` for an inferred or explicitly
named destination, so a bad ``code`` leaves every registry as it was
(:issue:`2186`). The check is on the type, not the value: the registry
enums compare equal to their values, so a member of another enum would
key the entry of the member it equals, e.g.
:attr:`TransType.ICMP <pcapkit.const.reg.transtype.TransType.ICMP>` would
replace :attr:`LinkType.ETHERNET <pcapkit.const.reg.linktype.LinkType.ETHERNET>`
(:issue:`2224`).
Args:
code: Registration key handed to the registrar.
kind: Enum the registrar is typed on, or :data:`None` for a registry
keyed by a number no enum names, which takes a bare :class:`int`
only (:issue:`2511`).
target: Protocol class whose registry ``code`` is destined for, named in
the error message so a rejection says *whose* registry rejected the
key rather than only what it wanted -- its callers share one
message, and several of them are typed on the same enum.
number: Whether a bare :class:`int` is a valid key too, as for the
registrars keyed on a port number or a payload protocol identifier.
Implied when ``kind`` is :data:`None`.
Raises:
RegistryError: If ``code`` is not a member of ``kind`` and, with
``number``, not an :class:`int` either. A member of another enum is
rejected even where an :class:`int` is accepted, and so is a
:class:`bool`, which would otherwise key ``0`` or ``1``
(:issue:`2269`).
"""
if kind is not None and isinstance(code, kind):
return
number = number or kind is None
member = isinstance(code, (enum.Enum, aenum.Enum))
if number and isinstance(code, int) and not isinstance(code, bool) and not member:
return
if kind is None:
expected = 'an int'
elif number:
expected = f'an int or a member of {kind.__name__}'
else:
expected = f'a member of {kind.__name__}'
if member:
raise RegistryError(f'{target.__name__}: code must be {expected}, not a member of '
f'{type(code).__name__}: {code!r}')
raise RegistryError(f'{target.__name__}: code must be {expected}, not {code!r}')