# -*- coding: utf-8 -*-
"""IPv6-Ext - IPv6 Extension Header
======================================
.. module:: pcapkit.protocols.internet.ipv6_ext.protocol
:mod:`pcapkit.protocols.internet.ipv6_ext.protocol` contains
:class:`~pcapkit.protocols.internet.ipv6_ext.protocol.IPv6_Ext`
only, which is both the shared **base class** of every IPv6 extension header
in this package and a **generic** extractor for a header with no dedicated
parser (:issue:`917`); the class docstring separates the 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.protocol.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
(: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.protocol.IPv6._import_next_layer`. Each still
enters :meth:`IPv6._decode_next_layer
<pcapkit.protocols.internet.ipv6.protocol.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.protocol.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 by direct dispatch only.
: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.Protocol._lookup_next_layer>`
would default it to plain :class:`~pcapkit.protocols.misc.raw.protocol.Raw`, whose
missing ``next`` field would end the walk in
:meth:`IPv6._decode_next_layer <pcapkit.protocols.internet.ipv6.protocol.IPv6._decode_next_layer>`
there (:issue:`891`). This module registers this class 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.
A header with a dedicated class whose parser raises does not reach this class.
:func:`~pcapkit.corekit.decorators.beholder` makes it plain
:class:`~pcapkit.protocols.misc.raw.protocol.Raw`, and the walk stops there, as
:rfc:`8200#section-4` has a node discard such a packet (:issue:`2477`).
The Overrun Guard
-----------------
The house convention for a declared length that does not fit what remains
is warn-and-clip: warn, naming what was declared against what is left, then
read only what is left (see
:func:`pcapkit.protocols.misc.pcapng.schema.bounded_area` and
:meth:`pcapkit.protocols.misc.pcapng.protocol.PCAPNG.read`).
This class keeps the warning -- a
:class:`~pcapkit.utilities.warnings.SchemaWarning`, as
:func:`~pcapkit.protocols.misc.pcapng.schema.bounded_area` emits -- but not the
clip. Here the declared length also decides *where the next header starts*: a
clipped skip distance points at the bytes that end the buffer, which are not a
header, and continuing would record a fabricated layer in
:class:`~pcapkit.corekit.protochain.ProtoChain`. So :meth:`read` warns, then
**stops the walk**: this instance absorbs every remaining octet and reports
:data:`None` for ``next``, which the caller's loop reads as "no more extension
headers".
"""
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.internet.internet import Internet
from pcapkit.protocols.internet.ipv6_ext.data import IPv6_Ext as Data_IPv6_Ext
from pcapkit.protocols.internet.ipv6_ext.schema import IPv6_Ext as Schema_IPv6_Ext
from pcapkit.protocols.misc.no_payload.protocol import NoPayload
from pcapkit.protocols.protocol import _PT, _ST, ProtocolBase
from pcapkit.protocols.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` identifies each extension header by the Next
Header field of the header before it, and every implemented one
records its own Next Header under this name, ESP from its decrypted
trailer -- which is what lets :attr:`IPv6_Ext.next` be a *shared* member
rather than a fallback-role-only one. Narrower than
:class:`~pcapkit.protocols.internet.ipv6_ext.data.IPv6_Ext` on
purpose: ``length`` is deliberately absent, because
:class:`~pcapkit.protocols.internet.ipv6_frag.data.IPv6_Frag` has no
such field (its length is the constant 8, and its own
:attr:`~pcapkit.protocols.internet.ipv6_frag.protocol.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, as ruled on :issue:`917`:
1. **The concrete fallback parser** for an :rfc:`6564`-conforming header
with no dedicated class, which is Shim6. :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.protocol.HOPOPT`,
:class:`~pcapkit.protocols.internet.ipv6_route.protocol.IPv6_Route`,
:class:`~pcapkit.protocols.internet.ipv6_frag.protocol.IPv6_Frag`,
:class:`~pcapkit.protocols.internet.ipv6_opts.protocol.IPv6_Opts`,
:class:`~pcapkit.protocols.internet.hip.protocol.HIP`,
:class:`~pcapkit.protocols.internet.mh.protocol.MH`,
:class:`~pcapkit.protocols.internet.ah.protocol.AH` and
:class:`~pcapkit.protocols.internet.esp.protocol.ESP`), which is what the
``_extf`` guards on :attr:`payload` and :attr:`protochain`, and each
subclass's own on ``protocol``, are for.
It is generic in its data and schema types, like
:class:`~pcapkit.protocols.internet.ipsec.protocol.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.protocol.AH` and
:class:`~pcapkit.protocols.internet.esp.protocol.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`, :meth:`__index__` and :meth:`id`
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, :meth:`__index__` would raise rather than
return its IANA number, and :meth:`id` would match every member's name
rather than narrow to its own class name. 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
.. versionchanged:: 1.6.0
* The library does not pass ``error`` to :meth:`read`, so
:attr:`Data_IPv6_Ext.error <pcapkit.protocols.internet.ipv6_ext.data.IPv6_Ext.error>`
is :data:`None` unless a caller passes one. A header whose dedicated
parser raises is :class:`~pcapkit.protocols.misc.raw.protocol.Raw`, whose
``info.error`` holds the error (:issue:`2477`).
* ``next`` of :meth:`make` takes a member or an :class:`int` code,
resolved with the enumeration's ``get``; a name, a :obj:`bool`, or a
code that does not fit in one octet raises
:exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than
being handed to the schema unchecked (:issue:`2594`).
* :meth:`from_data` keeps :attr:`protocol`, handing :meth:`read` the
``alias`` its parse took, rather than rebuilding it as :data:`None`
(:issue:`2768`).
"""
#: Every subclass overrides :meth:`make` with its own fields and forwards
#: nothing to this one, so this class's ``len`` and ``data`` are accepted
#: only by this class itself. Without the marker, a subclass whose ``make``
#: does not declare them would accept and then drop them (:issue:`2286`).
__own_make_keywords__ = True
#: :meth:`read` takes ``alias`` out of ``**kwargs`` by name, and
#: :meth:`_make_data` hands it back, so a rebuild keeps the :attr:`protocol`
#: its parse resolved (:issue:`2768`). Every subclass accepts it as well,
#: and drops it, as it does while parsing.
__keywords__ = frozenset({'alias'})
##########################################################################
# 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.protocol.IPv6_Frag.alias>` and
:attr:`IPv6_Opts.alias <pcapkit.protocols.internet.ipv6_opts.protocol.IPv6_Opts.alias>`,
for the same reason: :meth:`IPv6._decode_next_layer
<pcapkit.protocols.internet.ipv6.protocol.IPv6._decode_next_layer>` keys the
packet dict through ``IPv6._exthdr_key``, which is
``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.internet.ipv6_frag.data.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.Protocol.protocol>`
meaning ("name of next layer protocol"). As ruled on :issue:`891`, it
is repointed at this
instance's *own* identity, i.e. which header's format it parsed, which
for this class is only :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.protocol.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.Protocol.__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:`Protocol.protocol <pcapkit.protocols.protocol.Protocol.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]
@classmethod
def id(cls) -> 'tuple[Literal["HOPOPT"], Literal["IPv6_Route"], Literal["IPv6_Frag"], Literal["ESP"], Literal["AH"], Literal["IPv6_Opts"], Literal["MH"], Literal["HIP"]]': # pylint: disable=line-too-long
"""Index ID of the protocol.
Returns:
Index ID of every direct concrete subclass, each of which narrows
its own :meth:`id` to itself. A Shim6 header, which this class
parses, therefore matches every one of these names.
Note:
``== 'IPv6_Ext'`` is :data:`False`, as ``== 'IP'`` is on
:class:`~pcapkit.protocols.internet.ipv4.protocol.IPv4`; ``== IPv6_Ext`` is :data:`True`.
"""
return ('HOPOPT', 'IPv6_Route', 'IPv6_Frag', 'ESP', 'AH', 'IPv6_Opts', 'MH', 'HIP')
[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:
* ``alias`` -- the numeric code of the extension header this
instance parses, ``140`` for ``Shim6`` (the only code
registered to this class), supplied by :meth:`IPv6._import_next_layer
<pcapkit.protocols.internet.ipv6.protocol.IPv6._import_next_layer>`.
* ``error`` -- a parsing error to record in the result, if
any, which the library does not pass.
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:
# NOTE: As an IPv6 extension header the upper layer belongs to the enclosing
# IPv6 header, so hold NoPayload here so that membership does not raise (#2487).
self._next = NoPayload() # pylint: disable=attribute-defined-outside-init
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.
Raises:
ProtocolError: If ``next`` is a name, a :obj:`bool`, or a member
or code that does not fit in one octet.
Note:
**Declared, and keyword-only**, each for its own reason.
These keywords stay *declared*, unlike :meth:`read`'s own,
because ``_check_construction_keywords()`` builds its allowlist
from the :func:`inspect.signature` of ``make`` and the other
construction methods of every class in the MRO, plus any
``__keywords__``. 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 too, but a set
there is unioned down the MRO, so every subclass would accept
these names and drop them. A ``make`` signature can be kept from
subclasses instead: this class sets
:attr:`Protocol.__own_make_keywords__
<pcapkit.protocols.protocol.Protocol.__own_make_keywords__>`,
so a subclass that overrides ``make`` accepts ``len`` or ``data``
only where its own ``make`` declares it (:issue:`2286`).
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.protocol.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 for ``error``: it arrives on the
*parse* path alone, which ``__init__`` exempts from the
construction check. ``alias`` arrives on a rebuild too, from
:meth:`_make_data`, so this class lists it in
:attr:`~pcapkit.protocols.protocol.Protocol.__keywords__`
(:issue:`2768`).
"""
# 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=self._make_index(next, Enum_TransType, bits=8, name='next header'),
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.protocol.AH` and
:class:`~pcapkit.protocols.internet.esp.protocol.ESP` both default to
``version=4`` and are valid under IPv4, so an unconditional gate
would reject them (``ProtocolError: ESP: invalid version: must be
6, not 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.protocol.IPv4`
included, so that
:attr:`~pcapkit.const.ipv6.extension_header.ExtensionHeader.Shim6`
resolves to a working parser instead of
:class:`~pcapkit.protocols.misc.raw.protocol.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.protocol.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.protocol.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__}: invalid version: must be 6, not {version!r}')
#: 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)
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.protocol.Raw.__index__>`, which raises
because :class:`~pcapkit.protocols.misc.raw.protocol.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` parses
:attr:`~pcapkit.const.ipv6.extension_header.ExtensionHeader.Shim6`
(and any other RFC 6564-format code registered to it), 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. Hands
:meth:`read` the ``protocol`` it resolved as ``alias``, which picks
that rule and the identity again (:issue:`2768`).
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),
'alias': data.protocol,
}
# NOTE: Assigned directly rather than via ``code=``, whose registry import at class definition
# closes an import cycle through the half-built ``pcapkit.protocols.internet``.
Internet.__proto__[Enum_TransType.Shim6] = IPv6_Ext