Source code for pcapkit.foundation.traceflow.data.data

# -*- coding: utf-8 -*-
"""shared data models for flow tracing"""

from typing import TYPE_CHECKING

from pcapkit.corekit.infoclass import Info, info_final

__all__ = ['TraceFlowData', 'Deferred', 'DeferredPacket']

if TYPE_CHECKING:
    from typing import Any, Optional

    from pcapkit.foundation.reassembly.data.tcp import Datagram as TCP_Datagram
    from pcapkit.foundation.reassembly.tcp import TCP as TCP_Reassembly
    from pcapkit.foundation.traceflow.data.tcp import Index as TCP_Index


[docs] class Deferred: """A postponed reassembly of a traced flow's application layer. A traced flow's ``packet`` is the application-layer payload of the conversation, one datagram per direction. Producing it means reassembling the stream, which is neither free nor wanted by most callers of a *tracer* -- so the flow keeps the reassembler it was fed and this holds it until somebody reads :attr:`Index.packet <pcapkit.foundation.traceflow.data.tcp.Index.packet>`. Note: Deliberately not :class:`pcapkit.foundation.reassembly.data.data.Deferred`, and not shared with it. That one postpones a single ``analyze()`` call over bytes already in hand; this postpones a *submit* over a reassembler's buffers. The two subpackages are siblings and neither should depend on the other, so the twenty lines are written twice rather than one importing the other -- the same reason the two ``data/data.py`` modules mirror each other instead of merging. Args: reassembly: The flow's own :class:`~pcapkit.foundation.reassembly.tcp.TCP` reassembler, fed the segments of this conversation as they were traced. """ __slots__ = ('reassembly',) def __init__(self, reassembly: 'TCP_Reassembly') -> 'None': self.reassembly = reassembly
[docs] def __call__(self) -> 'tuple[TCP_Datagram, ...]': """Run the postponed reassembly. Returns: One reassembled datagram per direction of the conversation. Each carries its *own* postponed analysis in :attr:`Datagram.packet <pcapkit.foundation.reassembly.data.tcp.Datagram.packet>`, so parsing the payload as an application-layer protocol is still not paid for until that is read in turn. """ return self.reassembly.datagram
[docs] class DeferredPacket: """Resolves a :class:`Deferred` ``packet`` field on first read. The reading half of the arrangement above, and the counterpart of :class:`pcapkit.foundation.reassembly.data.data.DeferredPacket`. A subclass has to list ``packet`` in its ``__additional__``. That is what makes the field lazy at all: :class:`~pcapkit.corekit.infoclass.Info` stores a field named there under a mangled key and maps it back on the way out, so ``packet`` never lands in :attr:`~object.__dict__` itself -- which routes reading it through :meth:`__getattr__`, where the deferred reassembly can run, while ``dict(index)``, :meth:`to_dict` and iteration still report the field under its own name. """ # NOTE: the ``super()`` calls below are suppressed for both checkers. They are # undefined *on this mixin*, which is what a mixin is -- the base arrives at # the point of use, where every subclass is declared # ``class X(DeferredPacket, Info)`` and :class:`~pcapkit.corekit.infoclass.Info` # supplies all three. Neither mypy nor pylint can see that from here, and # pylint calls it an *error* rather than a warning.
[docs] def __analyse__(self) -> 'Optional[tuple[TCP_Datagram, ...]]': """Resolve a deferred reassembly, at most once. Returns: The flow's reassembled datagrams, or :data:`None` when the tracer was not asked to analyse the application layer. """ key = self.__map__.get('packet', 'packet') value = self.__dict__[key] if isinstance(value, Deferred): value = value() self.__dict__[key] = value return value
def __getattr__(self, name: 'str') -> 'Any': # NOTE: reached only for names absent from ``__dict__``, which ``packet`` # always is -- see ``__additional__`` above. Everything else has to raise, # or a typo would silently answer with a reassembled flow. if name != 'packet': raise AttributeError(f'{type(self).__name__!r} object has no attribute {name!r}') return self.__analyse__() def __getitem__(self, name: 'str') -> 'Any': if name == 'packet': return self.__analyse__() return super().__getitem__(name) # type: ignore[misc] # pylint: disable=no-member def __contains__(self, name: 'object') -> 'bool': # NOTE: ``Mapping.__contains__`` answers by fetching the value, which would # run the deferred reassembly merely to decide that the field exists. # ``packet`` is a declared field, so it is always there. return name == 'packet' or super().__contains__(name) # type: ignore[misc] # pylint: disable=no-member def __str__(self) -> 'str': self.__analyse__() return super().__str__() def __repr__(self) -> 'str': self.__analyse__() return super().__repr__()
[docs] def to_dict(self) -> 'dict[str, Any]': """Convert :class:`Index` into :obj:`dict`. Returns: The flow's fields, with ``packet`` reassembled if it had not been read yet -- a :obj:`dict` holding a :class:`Deferred` would leak an implementation detail into what is meant to be plain data. """ self.__analyse__() return super().to_dict() # type: ignore[misc] # pylint: disable=no-member
[docs] @info_final class TraceFlowData(Info): """Data storage for flow tracing.""" #: TCP traced flows. tcp: 'tuple[TCP_Index, ...]' if TYPE_CHECKING: def __init__(self, tcp: 'Optional[tuple[TCP_Index, ...]]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long