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
3 changes: 2 additions & 1 deletion docs/source/contributing/conventions/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ House Conventions
reference, rather than any code at all;
* :ref:`test-coverage` -- what the test suite has to prove;
* :ref:`utilities-or-corekit` -- which of :mod:`pcapkit.utilities` and
:mod:`pcapkit.corekit` a support module belongs in;
:mod:`pcapkit.corekit` a support module belongs in, and where a context
variable lives;
* :ref:`reassembly-integration` -- :mod:`pcapkit.foundation.reassembly` and what a
new engine there needs from :mod:`pcapkit.toolkit` and
:mod:`pcapkit.foundation.analysis`;
Expand Down
22 changes: 22 additions & 0 deletions docs/source/contributing/conventions/utilities-or-corekit.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,25 @@ checks that. What needs IPv4 and IPv6 stays beside
:class:`~pcapkit.protocols.misc.checksum.ip_pseudo.IP_Pseudo`. The package holds one
module per checksum family, so a new family sits beside
:mod:`~pcapkit.corekit.checksum.internet`.

Context Variables
~~~~~~~~~~~~~~~~~

Where a :class:`~contextvars.ContextVar` lives depends on which modules use it.
Settled by the owner on :issue:`2616`:

* **One that more than one module sets or reads lives in**
:mod:`pcapkit.utilities.contextvars`, under a public name in its ``__all__``. No
other module re-exports it, so every reader imports it from there.
* **One that only its own module uses stays in that module**, private, as
``_exthdr_rebuilt`` does in :mod:`pcapkit.protocols.internet.internet`.

A shared variable kept in one of the modules that use it makes the others import a
private name from that module, and ties them to its place in the import graph. A
module of bare declarations that imports nothing from :mod:`pcapkit` passes the
``utilities`` question above, so any module may import it --
:mod:`pcapkit.corekit.checksum` included, which must not reach
:mod:`pcapkit.protocols`. :file:`tests/project/test_shared_contextvars_2616_unit.py`
fails on a module that takes a context variable from anywhere else, by name or as an
attribute of an imported module, and on a variable there that is private, missing
from ``__all__``, or read by one module alone.
24 changes: 24 additions & 0 deletions docs/source/pcapkit/utilities/contextvars.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
Shared Context Variables
========================

.. module:: pcapkit.utilities.contextvars

:mod:`pcapkit.utilities.contextvars` holds the :class:`~contextvars.ContextVar`
objects that more than one :mod:`pcapkit` module sets or reads, so that no module
imports another's private state (:issue:`2616`). A context variable that only its
own module uses stays there. This module imports nothing from :mod:`pcapkit`, so
a module at any layer may import it; see :ref:`utilities-or-corekit`.

.. versionadded:: 1.6.0

.. autodata:: pcapkit.utilities.contextvars.beholding
:no-value:

.. autodata:: pcapkit.utilities.contextvars.frame_depth
:no-value:

.. autodata:: pcapkit.utilities.contextvars.in_fragment
:no-value:

.. autodata:: pcapkit.utilities.contextvars.rebuilding
:no-value:
4 changes: 3 additions & 1 deletion docs/source/pcapkit/utilities/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ Module Utilities
:mod:`pcapkit.utilities` holds the cross-cutting services :mod:`pcapkit` is
built with: user-refined exceptions and warnings, logging, the tunable limits
of :mod:`~pcapkit.utilities.config`, a compatibility layer, timestamp
conversion, and wrappers around third-party libraries. It imports nothing from
conversion, the :mod:`context variables <pcapkit.utilities.contextvars>` its
modules share, and wrappers around third-party libraries. It imports nothing from
:mod:`pcapkit` outside itself; see :ref:`utilities-or-corekit`.

.. toctree::
Expand All @@ -19,6 +20,7 @@ conversion, and wrappers around third-party libraries. It imports nothing from
config
chardet
datetime
contextvars

Version Compatibility
=====================
Expand Down
20 changes: 4 additions & 16 deletions pcapkit/corekit/checksum/internet.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@

"""
import contextlib
import contextvars
import enum
import struct
from typing import TYPE_CHECKING

from pcapkit.corekit.enum import EnumLookup
from pcapkit.utilities.contextvars import in_fragment, rebuilding
from pcapkit.utilities.exceptions import stacklevel
from pcapkit.utilities.warnings import ChecksumWarning, warn

Expand All @@ -43,18 +43,6 @@
'fragment_payload',
]

#: contextvars.ContextVar[bool]: Whether a payload is being rebuilt by
#: :meth:`Protocol._make_payload
#: <pcapkit.protocols.protocol.Protocol._make_payload>`, so that the layer
#: it is rebuilt for parses the same octets again; see
#: :data:`~pcapkit.utilities.config.FRAME_LAYER_LIMIT`. It lives here so that
#: :func:`verify_checksum` reads it without importing :mod:`pcapkit.protocols`.
_rebuilding = contextvars.ContextVar('_rebuilding', default=False)

#: contextvars.ContextVar[bool]: Whether the layer being parsed is the payload
#: of an IP fragment, see :func:`fragment_payload`.
_in_fragment = contextvars.ContextVar('_in_fragment', default=False)


class ChecksumStatus(EnumLookup, enum.IntEnum):
"""Outcome of verifying a checksum, as recorded in ``checksum_status``.
Expand Down Expand Up @@ -193,7 +181,7 @@ def verify_checksum(alias: 'str', data: 'bytes', field: 'bytes', *, length: 'Opt
value = int.from_bytes(field, 'big')
rest = total + (~value & 0xFFFF)
expected = ~((rest & 0xFFFF) + (rest >> 16)) & 0xFFFF
if not _rebuilding.get():
if not rebuilding.get():
warn(f'{alias}: checksum {value:#06x} is wrong; {expected:#06x} expected',
ChecksumWarning, stacklevel=stacklevel())
return ChecksumStatus.INVALID
Expand All @@ -212,8 +200,8 @@ def fragment_payload(fragment: 'bool') -> 'Iterator[None]':
fragment: Whether the IP layer is a fragment; an atomic one is not.

"""
token = _in_fragment.set(fragment)
token = in_fragment.set(fragment)
try:
yield
finally:
_in_fragment.reset(token)
in_fragment.reset(token)
14 changes: 3 additions & 11 deletions pcapkit/corekit/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@
decorators either.

"""
import contextvars
import functools
import io
import os
import traceback
from typing import TYPE_CHECKING, cast

from pcapkit.utilities.contextvars import beholding
from pcapkit.utilities.exceptions import (CaughtByCaller, ProtocolError, StreamEOFError,
StructError, stacklevel)
from pcapkit.utilities.logging import DEVMODE, VERBOSE, get_logger
Expand All @@ -55,14 +55,6 @@
#: :data:`pcapkit.utilities.logging.logger`.
logger = get_logger(__name__)

#: contextvars.ContextVar[bool]: Whether a layer that raises is discarded by its
#: caller, as :func:`beholder` replaces it and
#: :func:`~pcapkit.toolkit.pypcapfile._default_extension_header` drops it, so
#: that :meth:`Protocol._parse_next_layer
#: <pcapkit.protocols.protocol.Protocol._parse_next_layer>` drops its warnings
#: with it (:issue:`2308`).
_beholding = contextvars.ContextVar('_beholding', default=False)


def seekset(func: 'Callable[Concatenate[ProtocolBase, P], R_seekset]') -> 'Callable[P, R_seekset]':
"""Read file from start then set back to original.
Expand Down Expand Up @@ -183,14 +175,14 @@ def behold(*args: 'P.args', **kwargs: 'P.kwargs') -> 'R_beholder':

try:
# call method
token = _beholding.set(True)
token = beholding.set(True)
try:
# NOTE: The whole call is quiet for a ProtocolError, which the ``except`` below
# records; the recovery runs after it, loud unless an outer block is active (#2588).
with CaughtByCaller(ProtocolError):
return func(*args, **kwargs)
finally:
_beholding.reset(token)
beholding.reset(token)
except Exception as exc:
if isinstance(exc, StructError) and exc.eof: # pylint: disable=no-member
from pcapkit.protocols.misc.null import NoPayload as protocol # isort: skip # pylint: disable=import-outside-toplevel
Expand Down
5 changes: 3 additions & 2 deletions pcapkit/protocols/application/tls.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
from typing import TYPE_CHECKING, cast

from pcapkit.const.tls.content_type import ContentType as Enum_ContentType
from pcapkit.corekit.decorators import _beholding, beholder
from pcapkit.corekit.decorators import beholder
from pcapkit.protocols.application.ssl import (_TLS_VERSION_MAJOR, _WIRE_CONTENT_TYPES, SSL,
TLS13_VERSION)
from pcapkit.protocols.application.sslv2 import SSLv2
Expand All @@ -73,6 +73,7 @@
from pcapkit.protocols.data.application.sslv2 import SSLv2 as SSLv2_Data
from pcapkit.protocols.data.application.tls import TLS as Data_TLS
from pcapkit.protocols.schema.application.tls import TLS as Schema_TLS
from pcapkit.utilities.contextvars import beholding
from pcapkit.utilities.exceptions import ProtocolError
from pcapkit.utilities.warnings import hold_warnings

Expand Down Expand Up @@ -311,7 +312,7 @@ def unpack(self, length: 'Optional[int]' = None, *, chained: 'bool' = False, #
parsed = self._read_sslv2_fallback(len(self) if length is None else length, **kwargs)
fallback.discard = parsed is None
if parsed is None:
trial.discard = _beholding.get()
trial.discard = beholding.get()
raise
return parsed

Expand Down
5 changes: 3 additions & 2 deletions pcapkit/protocols/internet/icmpv4.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,9 @@ def read(self, length: 'Optional[int]' = None, *,
# an IP fragment carries only part of one.
if config.CHECKSUM_VERIFY:
from pcapkit.corekit.checksum.internet import ( # isort: skip # pylint: disable=import-outside-toplevel
ChecksumStatus, _in_fragment, verify_checksum)
if _in_fragment.get():
ChecksumStatus, verify_checksum)
from pcapkit.utilities.contextvars import in_fragment # isort: skip # pylint: disable=import-outside-toplevel
if in_fragment.get():
status = ChecksumStatus.NO_PSEUDO_HEADER
else:
status = verify_checksum(self.alias, self._data, schema.checksum, length=length)
Expand Down
4 changes: 2 additions & 2 deletions pcapkit/protocols/internet/internet.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@

from pcapkit.const.ipv6.extension_header import ExtensionHeader as Enum_ExtensionHeader
from pcapkit.const.reg.transtype import TransType as Enum_TransType
from pcapkit.corekit.checksum.internet import _rebuilding
from pcapkit.corekit.decorators import beholder
from pcapkit.corekit.module import ModuleDescriptor
from pcapkit.corekit.protochain import ProtoChain
from pcapkit.protocols.protocol import _PT, _ST, ProtocolBase
from pcapkit.protocols.schema.schema import keep_short_read, replay_short_read
from pcapkit.utilities import config
from pcapkit.utilities.contextvars import rebuilding
from pcapkit.utilities.exceptions import stacklevel
from pcapkit.utilities.warnings import ProtocolWarning, RegistryWarning, warn

Expand Down Expand Up @@ -428,7 +428,7 @@ class registered for ``proto``. Paired with it, the keywords that
# NOTE: Inside a payload being rebuilt, the layer it is rebuilt for
# warns of the same octets once its own parse gets here, as for
# :data:`~pcapkit.utilities.config.FRAME_LAYER_LIMIT`.
if not _rebuilding.get():
if not rebuilding.get():
warn(message, ProtocolWarning, stacklevel=stacklevel())
return Raw, {'error': message}

Expand Down
8 changes: 4 additions & 4 deletions pcapkit/protocols/internet/ipv4.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@
from pcapkit.const.ipv4.tos_thr import ToSThroughput as Enum_ToSThroughput
from pcapkit.const.ipv4.ts_flag import TSFlag as Enum_TSFlag
from pcapkit.const.reg.transtype import TransType as Enum_TransType
from pcapkit.corekit.checksum.internet import _rebuilding
from pcapkit.corekit.fields.ipaddress import parse_ip_address
from pcapkit.corekit.multidict import OrderedMultiDict
from pcapkit.protocols.data.internet.ipv4 import EOOLOption as Data_EOOLOption
Expand Down Expand Up @@ -106,6 +105,7 @@
from pcapkit.protocols.schema.internet.ipv4 import UnassignedOption as Schema_UnassignedOption
from pcapkit.protocols.schema.schema import Schema
from pcapkit.utilities import config
from pcapkit.utilities.contextvars import rebuilding
from pcapkit.utilities.exceptions import ProtocolError
from pcapkit.utilities.warnings import ProtocolWarning, RegistryWarning, warn

Expand Down Expand Up @@ -605,12 +605,12 @@ def make(self,
from pcapkit.protocols.misc.checksum.ip_pseudo import IP_Pseudo # isort: skip # pylint: disable=import-outside-toplevel

# NOTE: The header is parsed back for a source route's destination,
# quietly, as this layer parses it again (cf. _rebuilding).
token = _rebuilding.set(True)
# quietly, as this layer parses it again (cf. rebuilding).
token = rebuilding.set(True)
try:
header = IPv4(schema.pack()[:ihl * 4])
finally:
_rebuilding.reset(token)
rebuilding.reset(token)
# NOTE: The payload puts its own length in the pseudo-header.
pseudo = IP_Pseudo(ip=header, protocol=proto, upper_length=0)
upper = cast('ProtocolBase', payload)
Expand Down
12 changes: 6 additions & 6 deletions pcapkit/protocols/internet/ipv6.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@
from pcapkit.corekit.protochain import ProtoChain
from pcapkit.protocols.data.internet.ipv6 import IPv6 as Data_IPv6
from pcapkit.protocols.internet.ip import IP
from pcapkit.protocols.protocol import _frame_depth
from pcapkit.protocols.schema.internet.ipv6 import IP_MAX_LENGTH
from pcapkit.protocols.schema.internet.ipv6 import IPv6 as Schema_IPv6
from pcapkit.protocols.schema.internet.ipv6 import jumbo_payload_length, jumbogram_error
from pcapkit.utilities import config
from pcapkit.utilities.contextvars import frame_depth
from pcapkit.utilities.exceptions import ProtocolError

if TYPE_CHECKING:
Expand Down Expand Up @@ -521,7 +521,7 @@ def _decode_next_layer(self, ipv6: 'Data_IPv6', proto: 'Optional[int]' = None,
_exthdr = [] # type: list[tuple[Type[ProtocolBase], Any]] # (parser class, info) per extension header
split = None # type: Optional[tuple[int, int]] # at first Fragment header
opaque = False # walk ended at a non-zero Fragment Offset
depth = _frame_depth.get() # layers of the frame up to this one
depth = frame_depth.get() # layers of the frame up to this one

# traverse if next header is an extension header
payload = self.__header__.get_payload()
Expand Down Expand Up @@ -555,12 +555,12 @@ def _decode_next_layer(self, ipv6: 'Data_IPv6', proto: 'Optional[int]' = None,
# NOTE: Counted after every header walked, as it would be were each
# dissected by the one before it. ESP dissects its decrypted
# payload even when walked, and those layers count on from here.
token = _frame_depth.set(depth + len(_protos))
token = frame_depth.set(depth + len(_protos))
try:
next_ = self._import_next_layer(proto, packet=packet, version=6, extension=True,
payload=payload) # type: ignore[misc,call-arg]
finally:
_frame_depth.reset(token)
frame_depth.reset(token)
info = next_.info

# A layer with no ``next`` field ends the walk, as RFC 8200 section 4
Expand Down Expand Up @@ -656,14 +656,14 @@ def _decode_next_layer(self, ipv6: 'Data_IPv6', proto: 'Optional[int]' = None,
from pcapkit.corekit.checksum.internet import fragment_payload # isort: skip # pylint: disable=import-outside-toplevel
frags = self._exthdr.getlist(Enum_ExtensionHeader.IPv6_Frag) # type: ignore[arg-type]
scope = fragment_payload(any(frag.info.get('offset') or frag.info.get('mf') for frag in frags))
token = _frame_depth.set(depth + len(_protos))
token = frame_depth.set(depth + len(_protos))
try:
with scope:
return super()._decode_next_layer(ipv6, None if opaque else proto, raw_len, packet=packet,
ipv6_exthdr=ProtoChain.from_list(_protos), # type: ignore[arg-type]
payload=payload)
finally:
_frame_depth.reset(token)
frame_depth.reset(token)

@beholder # type: ignore[arg-type]
def _import_next_layer(self, proto: 'int', length: 'Optional[int]' = None, *, # pylint: disable=arguments-differ
Expand Down
Loading
Loading