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', 'FrameRecord']

if TYPE_CHECKING:
    from decimal import Decimal
    from typing import Any, Mapping, Optional, SupportsIndex

    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
    from pcapkit.protocols.misc.pcap.frame.data import FrameInfo as Data_FrameInfo


[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, as reassembled datagrams -- one per direction and acknowledgement number, see :class:`~pcapkit.foundation.traceflow.tcp.TCP`. 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 reassembly and flow tracing subpackages are siblings, and the flow tracer reaches the reassembler only lazily, when ``analyse`` asks for one, so the few lines are written in each rather than one importing the other -- the same reason their ``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: The conversation's reassembled datagrams, one per direction and acknowledgement number. 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
#: Each attribute a :class:`FrameRecord` carries beside its mapping, to the #: dunder key holding it, which iteration, ``len()`` and the dumps leave out. _RECORD = {'packet': '__packet__', 'frame_info': '__frame_info__', 'time_epoch': '__time_epoch__'}
[docs] @info_final class FrameRecord(Info): """A frame as a third-party engine's flow tracing adapter reports it. The mapping is the engine's own dissection of the frame, which every trace format that takes a mapping writes unchanged. Beside it, as attributes the mapping does not show, it carries what the PCAP trace dumper reads of a frame -- :meth:`PCAPIO._append_value <pcapkit.dumpkit.pcap.PCAPIO._append_value>` takes ``packet``, ``frame_info`` and ``time_epoch`` -- so ``trace_format='pcap'`` writes the frame's record too (:issue:`1507`). A key of the mapping and an attribute of the same name stay apart: ``record['packet']`` is the engine's, and ``record.packet`` the frame's captured octets. Args: mapping: The engine's dissection of the frame. packet: The frame's captured octets. frame_info: The frame's PCAP record header. time_epoch: The frame's exact UNIX timestamp. .. versionchanged:: 1.6.0 An :class:`~pcapkit.corekit.infoclass.Info` rather than a :obj:`dict` subclass, so it is immutable and no :obj:`dict`. :meth:`to_dict` exports the mapping alone, as any :class:`~pcapkit.corekit.infoclass.Info` exports its keys, and drops the ``packet``, ``frame_info`` and ``time_epoch`` attributes (:issue:`2726`). """ #: The dunder keys holding the attributes, which ``repr`` and ``str`` #: leave out as well. __excluded__ = list(_RECORD.values()) # NOTE: annotated for the type checker alone, since the attributes are no # fields of the mapping, which is the engine's. if TYPE_CHECKING: #: The frame's captured octets. packet: 'bytes' #: The frame's PCAP record header, with the fraction in the capture's own #: resolution. frame_info: 'Data_FrameInfo' #: The frame's exact UNIX timestamp, from which #: :meth:`PCAPIO._make_timestamp <pcapkit.dumpkit.pcap.PCAPIO._make_timestamp>` #: tells which resolution ``frame_info.ts_usec`` is in. A #: :class:`~decimal.Decimal`, since only that settles it exactly. time_epoch: 'Decimal' def __init__(self, mapping: 'Mapping[str, Any]', *, packet: 'bytes', frame_info: 'Data_FrameInfo', time_epoch: 'Decimal') -> 'None': super().__init__(mapping) self.__dict__.update(zip(_RECORD.values(), (packet, frame_info, time_epoch)))
[docs] def __getattribute__(self, name: 'str') -> 'Any': """Read an attribute, taking :attr:`packet`, :attr:`frame_info` and :attr:`time_epoch` from the keys holding them. So a mapping key of the same name never stands in for one of them. Args: name: Attribute name. Raises: AttributeError: If ``name`` is one of them and the record carries none, as one :meth:`from_dict` builds from a mapping. """ key = _RECORD.get(name) if key is None: return super().__getattribute__(name) try: return super().__getattribute__('__dict__')[key] except KeyError: raise AttributeError(f'{type(self).__name__!r} object has no attribute {name!r}') from None
[docs] def __reduce_ex__(self, protocol: 'SupportsIndex') -> 'tuple[Any, ...]': """Reduce to a call of :func:`_frame_record`, under every protocol. A release before 1.6.0 reduces a record to it as well, so a pickle loads on either side of 1.6.0. Args: protocol: Pickle protocol in use, which does not change the reduction. Returns: ``(_frame_record, (mapping, packet, frame_info, time_epoch))``. """ return _frame_record, (dict(self), self.packet, self.frame_info, self.time_epoch)
def _frame_record(mapping: 'Mapping[str, Any]', packet: 'bytes', frame_info: 'Data_FrameInfo', time_epoch: 'Decimal') -> 'FrameRecord': """Rebuild a pickled :class:`FrameRecord`. Its pickles call this under every protocol, as those of a release before 1.6.0 do. Args: mapping: The engine's dissection of the frame. packet: The frame's captured octets. frame_info: The frame's PCAP record header. time_epoch: The frame's exact UNIX timestamp. Returns: The frame record. """ return FrameRecord(mapping, packet=packet, frame_info=frame_info, time_epoch=time_epoch)
[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