Source code for pcapkit.protocols.internet.ipv6_ext

# -*- coding: utf-8 -*-
"""IPv6-Ext - IPv6 Extension Header
================================

.. module:: pcapkit.protocols.internet.ipv6_ext

:mod:`pcapkit.protocols.internet.ipv6_ext` contains
:class:`~pcapkit.protocols.internet.ipv6_ext.IPv6_Ext`
only, which is both (GitHub issue :issue:`917`) the shared **base class** of
every IPv6 extension header in this package and a **generic** extractor that
stands in for a header whose dedicated parser is unavailable or has failed.
The class docstring separates these roles.

Length Rules
------------

:rfc:`6564#section-4` is Standards Track and says, with an RFC 2119
**MUST**, that any IPv6 extension header defined from April 2012
onward carries the same first two octets:

======= ========= ===================== =====================================
Octets      Bits        Name                    Description
======= ========= ===================== =====================================
  0           0   ``next``                    Next Header
  1           8   ``len``                     Hdr Ext Len (8-octet units,
                                              excluding the first 8 octets)
  2          16   ``payload``                 Header-specific content
======= ========= ===================== =====================================

so the first two octets of a *conforming* header are parseable without
knowing anything else about it. :rfc:`6564#section-5` is explicit that
this is **not retroactive** -- it applies only to newly defined extension
headers -- which is why the pre-existing headers need the closed exception
table below rather than being assumed to conform. (:rfc:`8200#section-4.8`
restates the layout without a MUST and mislabels the generic length field as
the length of the Destination Options header; cite :rfc:`6564#section-4`
instead.)

The exception table, verified against IANA's ``protocol-numbers-1.csv``
*IPv6 Extension Header* column and each cited RFC:

===================================================================== =================================================
Header(s)                                                             Length rule
===================================================================== =================================================
``HOPOPT``, ``IPv6-Route``, ``IPv6-Opts``, ``MH``, ``HIP``, ``Shim6`` ``(octet[1] + 1) * 8`` -- the :rfc:`6564`
                                                                      generic rule
``IPv6-Frag``                                                         constant ``8``; octet[1] is Reserved, not a
                                                                      length (:rfc:`8200#section-4.5`)
``AH``                                                                ``(octet[1] + 2) * 4`` -- :rfc:`4302#section-2.2`
                                                                      counts in 4-octet units with a bias of 2,
                                                                      not :rfc:`6564`'s 8-octet units and bias of 1
``ESP``                                                               terminal -- has a dedicated, registered parser
                                                                      (:class:`~pcapkit.protocols.internet.esp.ESP`),
                                                                      but its Next Header byte is inside the
                                                                      encrypted trailer (:rfc:`4303`), so the walk
                                                                      cannot step past it on the wire
``253``, ``254``                                                      terminal -- no dedicated parser exists, so no
                                                                      next header field is ever read at all, either
===================================================================== =================================================

This table classifies by *wire format* alone, over every code
:class:`~pcapkit.const.ipv6.extension_header.ExtensionHeader` enumerates,
which match IANA's *IPv6 Extension Header Types* registry exactly (GitHub
issue :issue:`925`).

``Shim6`` conforms to the generic rule (:rfc:`5533`) but has no dedicated parser class; see
"Entry Paths" below for how it reaches this class by direct dispatch.

:rfc:`8200#section-4.5` sets Encapsulating Security Payload aside: ESP is not
an extension header, and the RFC lists it among upper-layer headers.
:rfc:`4303` puts its Next Header inside the encrypted trailer, with no length
field in the cleartext part. 253 and 254 are reserved for private
experimentation (:rfc:`3692`) with no wire format at all. None of them
is reachable through this class, by construction; see
:meth:`pcapkit.protocols.internet.ipv6.IPv6._import_next_layer`. Each still
enters :meth:`IPv6._decode_next_layer
<pcapkit.protocols.internet.ipv6.IPv6._decode_next_layer>`'s walk, being a
real :class:`~pcapkit.const.ipv6.extension_header.ExtensionHeader` member, but
they part ways there. ``ESP`` resolves to its own dedicated parser, which
decodes any plaintext it recovers itself and whose info carries a ``next`` of
:data:`None` whenever the payload stays opaque, so the walk ends the ordinary
way with ``ExtensionHeader(None)`` failing at the top of the next iteration. ``253``
and ``254`` have no dedicated parser and resolve to plain
:class:`~pcapkit.protocols.misc.raw.Raw`, whose info has no ``next``
*attribute*. For these two, and any future IANA code without an
implementation, the walk stops on a *structural* check (does the parsed layer
carry a ``next`` at all?) rather than on a list of codes.

Entry Paths
-----------

This class is reached in these ways:

1. **An unrecognised protocol number.** :class:`~pcapkit.const.ipv6.extension_header.ExtensionHeader.Shim6`
   is a real IANA extension header (:rfc:`5533`) with no dedicated parser, so
   :meth:`Internet._lookup_next_layer <pcapkit.protocols.protocol.ProtocolBase._lookup_next_layer>`
   would default it to plain :class:`~pcapkit.protocols.misc.raw.Raw`. That
   has no ``next`` field, so the walk in
   :meth:`IPv6._decode_next_layer <pcapkit.protocols.internet.ipv6.IPv6._decode_next_layer>`
   would crash on it (GitHub issue :issue:`891`). This module registers itself
   for that code instead (see the bottom of the module), so dispatch reaches a
   working generic parser directly. The registration is global, shared by
   every :class:`~pcapkit.protocols.internet.internet.Internet` subclass, so
   :meth:`__post_init__` gates on ``version == 6`` to keep it from also
   activating for an IPv4 payload that carries protocol number 140; see its
   docstring.
2. **A recognised header whose own parser raises.** ``HOPOPT``,
   ``IPv6-Route``, ``IPv6-Opts``, ``MH``, ``HIP``, ``IPv6-Frag`` and ``AH`` all
   have dedicated classes. When one of *those* raises,
   :func:`~pcapkit.corekit.decorators.beholder`
   (:meth:`Protocol._import_next_layer <pcapkit.protocols.protocol.ProtocolBase._import_next_layer>`)
   would substitute plain :class:`~pcapkit.protocols.misc.raw.Raw`, which loses
   ``next`` and crashes the walk as in path 1.
   :meth:`IPv6._import_next_layer <pcapkit.protocols.internet.ipv6.IPv6._import_next_layer>`
   catches the failure itself, one layer in from ``beholder``, and substitutes
   this class: the bad header costs only itself, not the rest of the chain.

The Overrun Guard
-----------------

The house convention for a declared length that does not fit what remains
is warn-and-clip: emit a warning naming what was declared against what is
left, then read only what is left. See
:func:`pcapkit.protocols.schema.misc.pcapng.bounded_area` and
:meth:`pcapkit.protocols.misc.pcapng.PCAPNG.read`.

This class follows that convention's *warning*
(:class:`~pcapkit.utilities.warnings.SchemaWarning`) and deliberately diverges
on the *action*. Clipping is right when the declared length only governs how
much of the current object to read, since there is always a well-defined
"what is left" to fall back to. Here the declared length also decides *where
the next header starts*; a clipped skip distance points at whatever bytes
end the buffer, which are not a header. Continuing from there would
fabricate a layer and record a false entry in
:class:`~pcapkit.corekit.protochain.ProtoChain`, which reads as a parsed fact
rather than a guess. So :meth:`read` warns in the same wording as the sites
above, then **stops the walk**: this instance absorbs every remaining octet
and reports :data:`None` for ``next``, which the caller's loop already reads
as "no more extension headers". The chain ends honestly at the bad header
instead of inventing what follows it.

"""
from typing import TYPE_CHECKING, Generic, cast, overload

from pcapkit.const.ipv6.extension_header import ExtensionHeader as Enum_ExtensionHeader
from pcapkit.const.reg.transtype import TransType as Enum_TransType
from pcapkit.protocols.data.internet.ipv6_ext import IPv6_Ext as Data_IPv6_Ext
from pcapkit.protocols.internet.internet import Internet
from pcapkit.protocols.protocol import _PT, _ST, ProtocolBase
from pcapkit.protocols.schema.internet.ipv6_ext import IPv6_Ext as Schema_IPv6_Ext
from pcapkit.protocols.schema.schema import Schema
from pcapkit.utilities.exceptions import ProtocolError, UnsupportedCall, stacklevel
from pcapkit.utilities.warnings import SchemaWarning, warn

if TYPE_CHECKING:
    from typing import IO, Any, NoReturn, Optional, Protocol

    from typing_extensions import Literal

    from pcapkit.corekit.protochain import ProtoChain

    class _NextHeaderData(Protocol):
        """The one field every IPv6 extension header's data model carries.

        :rfc:`8200#section-4.1` puts a Next Header octet first in every
        extension header, and every implemented one records it under this
        name -- which is what lets :attr:`IPv6_Ext.next` be a *shared* member
        rather than a fallback-role-only one. Narrower than
        :class:`~pcapkit.protocols.data.internet.ipv6_ext.IPv6_Ext` on
        purpose: ``length`` is deliberately absent, because
        :class:`~pcapkit.protocols.data.internet.ipv6_frag.IPv6_Frag` has no
        such field (its length is the constant 8, and its own
        :attr:`~pcapkit.protocols.internet.ipv6_frag.IPv6_Frag.length`
        property supplies it).

        """

        next: 'Optional[Enum_TransType]'

__all__ = ['IPv6_Ext']


[docs] class IPv6_Ext(Internet[_PT, _ST], Generic[_PT, _ST], schema=Schema_IPv6_Ext, data=Data_IPv6_Ext): """This class implements a generic IPv6 extension header parser, and is the shared base of every IPv6 extension header in this package. The module documentation has the RFC citations behind the :ref:`length rules <ipv6-ext-length-rules>`, the :ref:`ways this class is dispatched to <ipv6-ext-entry-paths>`, and :ref:`why an overrun stops the walk <ipv6-ext-overrun-guard>` rather than clipping. .. rubric:: Base and Fallback Roles One class plays both, on the owner's ruling for GitHub issue :issue:`917`: 1. **The concrete fallback parser** for an :rfc:`6564`-conforming header with no dedicated class, or whose dedicated class raised. :meth:`read`, :meth:`make`, :attr:`name`, :attr:`alias` and the ``schema=``/``data=`` class keywords implement it. 2. **The base class** of the implemented extension headers (:class:`~pcapkit.protocols.internet.hopopt.HOPOPT`, :class:`~pcapkit.protocols.internet.ipv6_route.IPv6_Route`, :class:`~pcapkit.protocols.internet.ipv6_frag.IPv6_Frag`, :class:`~pcapkit.protocols.internet.ipv6_opts.IPv6_Opts`, :class:`~pcapkit.protocols.internet.hip.HIP`, :class:`~pcapkit.protocols.internet.mh.MH`, :class:`~pcapkit.protocols.internet.ah.AH` and :class:`~pcapkit.protocols.internet.esp.ESP`), which is what the ``_extf`` guards on :attr:`payload`, :attr:`protocol` and :attr:`protochain` are for. It is generic in its data and schema types, like :class:`~pcapkit.protocols.internet.ipsec.IPsec`, another base in this package, so that a subclass keeps its *own* ``_PT``/``_ST`` instead of inheriting this class's. :class:`~pcapkit.protocols.internet.ah.AH` and :class:`~pcapkit.protocols.internet.esp.ESP` therefore double-inherit two identically-parameterised generic bases, ``IPsec[…]`` and ``IPv6_Ext[…]``. Warning: A subclass **must** define :attr:`name`, :attr:`alias`, :attr:`protocol`, :attr:`length` and :meth:`__index__` itself. Each carries this class's *fallback-role* answer, which is wrong for a header with an identity of its own: it would report itself as ``IPv6 Extension Header`` / ``IPv6-Ext``, read its length off a data model that is not its own, and :meth:`__index__` would raise rather than return its IANA number. The language cannot enforce the override, so ``tests/protocols/internet/test_ipv6_ext_unit.py`` does, over every subclass discovered at runtime. .. versionadded:: 1.5.0 """ ########################################################################## # Properties. ########################################################################## @property def name(self) -> 'str': """Name of current protocol. Annotated ``str`` rather than the ``Literal`` every *leaf* protocol in this package uses, because this one is also a base: a ``Literal`` here makes each subclass's own ``Literal`` an incompatible override (mypy ``[override]``). The value is still the single fallback-role constant. """ return 'IPv6 Extension Header' @property def alias(self) -> 'str': """Acronym of corresponding protocol. Annotated ``str`` rather than ``Literal``, for the reason given on :attr:`name`. Hyphenated, like :attr:`IPv6_Frag.alias <pcapkit.protocols.internet.ipv6_frag.IPv6_Frag.alias>` and :attr:`IPv6_Opts.alias <pcapkit.protocols.internet.ipv6_opts.IPv6_Opts.alias>`, for the same reason: :meth:`IPv6._decode_next_layer <pcapkit.protocols.internet.ipv6.IPv6._decode_next_layer>` builds the packet-dict key with ``self.alias.lstrip('IPv6-').lower()``, and :meth:`str.lstrip` strips a character *set*, not a prefix. The default (class-name) alias ``'IPv6_Ext'`` would strip to ``'_Ext'`` and key the dict as ``_ext``; the hyphen makes every leading character (``I``, ``P``, ``v``, ``6``, ``-``) part of the stripped set, giving ``ext``. """ return 'IPv6-Ext' @property def length(self) -> 'int': """Header length of current protocol. Fallback-role member: it reads this module's *own* data model, so a subclass whose data model records its length elsewhere, or not at all (as :class:`~pcapkit.protocols.data.internet.ipv6_frag.IPv6_Frag`), must override it. Every implemented header does, and ``tests/protocols/internet/test_ipv6_ext_unit.py`` holds them to it. """ return cast('Data_IPv6_Ext', self._info).length @property def protocol(self) -> 'Optional[Enum_ExtensionHeader] | Optional[str]': """The extension header this instance stands in for. In the *fallback* role (this instance parsed this module's own schema) this is **not** the base :attr:`Protocol.protocol <pcapkit.protocols.protocol.ProtocolBase.protocol>` meaning ("name of next layer protocol"). On the owner's ruling for GitHub issue :issue:`891` it is deliberately repointed at this instance's *own* identity, i.e. which header's format it parsed, such as :attr:`~pcapkit.const.ipv6.extension_header.ExtensionHeader.HOPOPT` or :attr:`~pcapkit.const.ipv6.extension_header.ExtensionHeader.Shim6`. The identity is resolved per instance from the numeric code handed to the constructor (``alias``), which :meth:`pcapkit.protocols.internet.ipv6.IPv6._decode_next_layer` already holds before it dispatches. See :meth:`__index__` for why the *class-level* identity cannot work the same way. It is deliberately *not* ``_extf``-guarded in this role: that same call passes ``extension=True`` for every extension header in a chain, so guarding it would make the identity unreadable in exactly the case it exists for. In the *base* role it falls through to ``super()``, restoring the ordinary :class:`~pcapkit.protocols.protocol.ProtocolBase` meaning. That is load-bearing: every subclass implements its own ``_extf``-guarded ``protocol`` as ``return super().protocol``, and this class sits between them and :class:`~pcapkit.protocols.protocol.ProtocolBase` in the MRO, so without the discriminator below each would read ``self._info.protocol`` off a data model with no such field (``AttributeError``). The discriminator is the *class-level* :attr:`~pcapkit.protocols.protocol.ProtocolBase.__data__` rather than an :func:`isinstance` test on ``self._info``, because a ``make``-only instance has no ``_info`` at all. Reading it would turn :attr:`ProtocolBase.protocol <pcapkit.protocols.protocol.ProtocolBase.protocol>`, which only needs ``self._protos``, into an ``AttributeError``; ``tests/protocols/internet/test_ipv6_extension_unit.py`` constructs exactly that instance. """ if self.__data__ is Data_IPv6_Ext: return cast('Data_IPv6_Ext', self._info).protocol return super().protocol @property def next(self) -> 'Optional[Enum_TransType]': """Next header, as parsed off the wire. Shared by every subclass rather than fallback-role-only, since each of their data models records the Next Header value under this name. :data:`None` when the declared length would have overrun what remained of the chain and the walk stopped instead of trusting it; see :ref:`ipv6-ext-overrun-guard`. """ return cast('_NextHeaderData', self._info).next @property def payload(self) -> 'ProtocolBase | NoReturn': """Payload of current instance. Raises: UnsupportedCall: if the protocol is used as an IPv6 extension header """ if self._extf: raise UnsupportedCall(f"'{self.__class__.__name__}' object has no attribute 'payload'") return super().payload @property def protochain(self) -> 'ProtoChain | NoReturn': """Protocol chain of current instance. Raises: UnsupportedCall: if the protocol is used as an IPv6 extension header """ if self._extf: raise UnsupportedCall(f"'{self.__class__.__name__}' object has no attribute 'protochain'") return super().protochain ########################################################################## # Methods. ##########################################################################
[docs] def read(self, length: 'Optional[int]' = None, *, # pylint: disable=arguments-differ extension: 'bool' = False, **kwargs: 'Any') -> '_PT': """Read a generically-parsed IPv6 extension header. Args: length: Length of packet data. extension: If the protocol is used as an IPv6 extension header. **kwargs: Arbitrary keyword arguments, including these, which are this class's own and are supplied by :meth:`IPv6._import_next_layer <pcapkit.protocols.internet.ipv6.IPv6._import_next_layer>` rather than typed by a caller: * ``alias`` -- the numeric extension header code this instance stands in for, e.g. ``0`` for ``HOPOPT`` or ``140`` for ``Shim6``. * ``error`` -- the parsing error, when this instance was reached as a :func:`~pcapkit.corekit.decorators.beholder` fallback rather than by direct dispatch. Returns: Parsed packet data. Note: These are read out of ``**kwargs`` rather than declared, and ``version`` is not declared either, because this class is *also* the base of subclasses whose own ``read`` accepts none of them. Declaring them would assert of the whole family an interface only the fallback has, which mypy (``Signature of "read" incompatible with supertype``, ``[override]``) and pylint (``arguments-differ``) both reject. This method never reads ``version``; the version gate lives in :meth:`__post_init__`, which still takes it explicitly. """ alias = kwargs.get('alias') # type: Optional[int] error = kwargs.get('error') # type: Optional[Exception] if length is None: length = len(self) schema = cast('Schema_IPv6_Ext', self.__header__) ext_code = None # type: Optional[Enum_ExtensionHeader] if alias is not None: try: ext_code = Enum_ExtensionHeader(alias) except ValueError: ext_code = None if ext_code == Enum_ExtensionHeader.IPv6_Frag: # RFC 8200 §4.5: octet 1 is Reserved, not a length; the fragment # header is always exactly 8 octets, and RFC 6564 §5 says it predates # and does not follow the generic format. nominal = 8 elif ext_code == Enum_ExtensionHeader.AH: # RFC 4302 §2.2: Payload Len is in 4-octet units, excluding the # first 8 octets: a different unit and bias from RFC 6564's # Hdr Ext Len. nominal = (schema.len + 2) * 4 else: # RFC 6564 §4 (MUST): Hdr Ext Len is in 8-octet units, excluding # the first 8 octets. Also the best generic guess when ``alias`` # named no known extension header. nominal = (schema.len + 1) * 8 if nominal > length: # Warns like the house convention but stops rather than clips; see # the module docstring's "The Overrun Guard" section. warn(f'IPv6: extension header declares a length of {nominal} octet(s) with ' f'{length} octet(s) left in the chain; stopping the walk instead of ' f'skipping past it', SchemaWarning, stacklevel=stacklevel()) ext_len = length next_header = None # type: Optional[Enum_TransType] else: ext_len = nominal next_header = schema.next # NOTE: The schema's payload holds every octet after the two fixed # ones; the first ``ext_len - 2`` of them are this header's own body. rest = schema.get_payload() generic_ext = Data_IPv6_Ext( protocol=ext_code, next=next_header, declared_next=schema.next, len=schema.len, length=ext_len, data=rest[:max(ext_len - 2, 0)], error=error, ) if extension: return cast('_PT', generic_ext) return self._decode_next_layer(cast('_PT', generic_ext), next_header, length - ext_len, payload=rest[max(ext_len - 2, 0):])
[docs] def make(self, *, next: 'Enum_TransType | int' = Enum_TransType.UDP, # pylint: disable=redefined-builtin len: 'int' = 0, # pylint: disable=redefined-builtin data: 'bytes' = b'', payload: 'bytes | ProtocolBase | Any' = b'', **kwargs: 'Any') -> '_ST': """Make (construct) packet data. Args: next: Next header type. len: Raw ``Hdr Ext Len`` octet to emit. The caller must size it, since this class cannot know at construction time which per-protocol rule (see :ref:`ipv6-ext-length-rules`) it must satisfy. data: Header-specific content after the two fixed octets, emitted verbatim ahead of ``payload``. payload: Payload of current instance. **kwargs: Arbitrary keyword arguments. Returns: Constructed packet data. Note: **Declared, and keyword-only**, each for its own reason. These keywords stay *declared*, unlike :meth:`read`'s own, because ``_check_construction_keywords()`` builds its allowlist from :func:`inspect.signature` of ``make``. Hiding them in ``**kwargs`` **breaks construction**: a plain ``IPv6_Ext(next=..., len=..., data=..., payload=...)`` raises ``UnsupportedCall: IPv6_Ext: unexpected keyword(s)``. The ``__keywords__`` escape hatch would cover that, but it is unioned down the MRO, so it would widen the allowlist, and weaken that misspelling check, for every subclass too. They are keyword-*only* because this class is also a base and each subclass's ``make`` puts different names in the same positions. As positional parameters they draw pylint ``arguments-renamed`` warnings (:meth:`ESP.make <pcapkit.protocols.internet.esp.ESP.make>` has ``spi``, ``seq`` and ``next`` where these have ``next``, ``len`` and ``data``) and mypy ``[override]`` errors. With no positional parameters there is no position to disagree about, and the allowlist is unaffected because :func:`~pcapkit.protocols.protocol._declared_keywords` collects ``KEYWORD_ONLY`` parameters too. Nothing calls a protocol's ``make`` positionally; ``__init__`` spreads ``**kwargs`` into it. ``read`` needs none of this: its ``alias``/``error`` only arrive on the *parse* path, which ``__init__`` exempts from the construction check. """ # NOTE: The schema keeps everything after the two fixed octets as one # opaque payload, so the body is packed in front of the upper layer. if data: if isinstance(payload, ProtocolBase): payload = bytes(payload) elif isinstance(payload, Schema): payload = payload.pack() payload = data + payload return cast('_ST', Schema_IPv6_Ext( next=next, len=len, payload=payload, ))
########################################################################## # Data models. ########################################################################## @overload def __post_init__(self, file: 'IO[bytes] | bytes', length: 'Optional[int]' = ..., *, # pylint: disable=arguments-differ version: 'Literal[4, 6]' = ..., extension: 'bool' = ..., **kwargs: 'Any') -> 'None': ... @overload def __post_init__(self, **kwargs: 'Any') -> 'None': ... # pylint: disable=arguments-differ
[docs] def __post_init__(self, file: 'Optional[IO[bytes] | bytes]' = None, length: 'Optional[int]' = None, *, # pylint: disable=arguments-differ version: 'Literal[4, 6]' = 6, extension: 'bool' = False, **kwargs: 'Any') -> 'None': """Post initialisation hook. Args: file: Source packet stream. length: Length of packet data. version: IP protocol version. extension: If the protocol is used as an IPv6 extension header. **kwargs: Arbitrary keyword arguments. Raises: ProtocolError: If this is the *fallback* parser (see the class docstring's "Base and Fallback Roles") and ``version`` is not ``6``. See Also: For construction arguments, please refer to :meth:`make`. Note: The version gate is scoped to the fallback role by the same ``__data__`` discriminator :attr:`protocol` uses. Applying it to subclasses would break those that are *not* IPv6-only: :class:`~pcapkit.protocols.internet.ah.AH` and :class:`~pcapkit.protocols.internet.esp.ESP` both default to ``version=4`` and are valid under IPv4, so an unconditional gate would reject them (``ProtocolError: ESP: only valid for IPv6, got version=4``). The gate exists because this class is registered into :attr:`Internet.__proto__ <pcapkit.protocols.internet.internet.Internet.__proto__>`, which *every* :class:`~pcapkit.protocols.internet.internet.Internet` subclass shares, :class:`~pcapkit.protocols.internet.ipv4.IPv4` included, so that :attr:`~pcapkit.const.ipv6.extension_header.ExtensionHeader.Shim6` resolves to a working parser instead of :class:`~pcapkit.protocols.misc.raw.Raw`. Without it, an IPv4 packet whose protocol byte is 140 would reach this class and walk an IPv6-style extension-header chain out of an IPv4 payload, reading ``IPv4:IPv6-Ext:...`` instead of the ``IPv4:Shim6`` that a plain, non-continuing :class:`~pcapkit.protocols.misc.raw.Raw` gives. Rejecting here sends construction back through :func:`~pcapkit.corekit.decorators.beholder` at the *caller's* layer, which substitutes that same :class:`~pcapkit.protocols.misc.raw.Raw`: the version-agnostic behaviour, with no IPv4-specific code. """ if self.__data__ is Data_IPv6_Ext and version != 6: raise ProtocolError( f'{self.__class__.__name__}: only valid for IPv6, got version={version}') #: bool: If the protocol is used as an IPv6 extension header. self._extf = extension # call super __post_init__ super().__post_init__(file, length, version=version, extension=extension, **kwargs) # type: ignore[arg-type]
def __length_hint__(self) -> 'int': """Return an estimated length for the object. Eight octets, the shortest header :rfc:`6564#section-4` allows: Hdr Ext Len counts 8-octet units "not including the first 8 octets", and :rfc:`8200#section-4` makes every extension header a multiple of 8 octets. Annotated ``int`` rather than ``Literal[8]``, for the reason given on :attr:`name`: every subclass has its own fixed header and its own ``Literal``. """ return 8
[docs] @classmethod def __index__(cls) -> 'NoReturn': """Numeral registry index of the protocol. Raises: UnsupportedCall: This protocol has no *class-level* registry entry. Unlike :meth:`Raw.__index__ <pcapkit.protocols.misc.raw.Raw.__index__>`, which raises because :class:`~pcapkit.protocols.misc.raw.Raw` has no identity to report at all, this class *does* have one -- see :attr:`protocol` -- but it is resolved per instance, not per class: one :class:`IPv6_Ext` stands in for :attr:`~pcapkit.const.ipv6.extension_header.ExtensionHeader.HOPOPT`, :attr:`~pcapkit.const.ipv6.extension_header.ExtensionHeader.Shim6` and any other RFC 6564-conforming code alike, while :meth:`__index__` is a ``@classmethod`` with nowhere to put a value that differs per instance. """ raise UnsupportedCall(f'{cls.__name__!r} object cannot be interpreted as an integer')
########################################################################## # Utilities. ##########################################################################
[docs] @classmethod def _make_data(cls, data: 'Data_IPv6_Ext') -> 'dict[str, Any]': # type: ignore[override] """Create key-value pairs from ``data`` for protocol construction. Emits the ``Hdr Ext Len`` octet and the body as they were read, so the rebuild is byte-exact whichever per-protocol length rule :meth:`read` applied, and also for an overrun, where ``data.next`` is :data:`None` and ``data.declared_next`` keeps the octet. Args: data: protocol data Returns: Key-value pairs for protocol construction. """ return { 'next': data.declared_next, 'len': data.len, 'data': data.data, 'payload': cls._make_payload(data), }
# NOTE: Registered by direct assignment into ``Internet.__proto__``, as # ``pcapkit.protocols.internet.internet`` does for every other entry, rather # than via the ``code=`` keyword of ``ProtocolBase.__init_subclass__``. The two # are equivalent in effect, but ``code=`` resolves through # ``register_protocol_code``, which imports # ``pcapkit.foundation.registry.protocols`` *at class-definition time*, while # ``pcapkit.protocols.internet`` (which imports this module) is still being # built. From here that completes a cycle back into the unfinished # ``pcapkit.protocols.internet`` through ``pcapkit.foundation.extraction``: # ``ImportError: cannot import name 'Extractor' from partially initialized # module 'pcapkit.foundation.extraction'`` on a bare ``import pcapkit``. Plain # dict assignment carries no import of its own. Internet.__proto__[Enum_TransType.Shim6] = IPv6_Ext