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
4 changes: 4 additions & 0 deletions docs/source/pcapkit/protocols/misc/pcapng.rst
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,10 @@ Data Models
:members:
:show-inheritance:

.. autoclass:: pcapkit.protocols.data.misc.pcapng.TLSKeyLogEntries
:members:
:show-inheritance:

.. autoclass:: pcapkit.protocols.data.misc.pcapng.TLSKeyLog
:members:
:show-inheritance:
Expand Down
4 changes: 2 additions & 2 deletions docs/source/pcapkit/utilities/pickle.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ Pickle Reductions

:mod:`pcapkit.utilities.pickle` holds the :mod:`pickle` reductions several
:mod:`pcapkit` classes share, so each is written once (:issue:`2714`).
:class:`ReduceAsProtocol2` lets a :class:`typing.Generic` subclass or a slotted
:class:`dict` pickle under protocol 0 and 1, on GraalPy as on CPython;
:class:`ReduceAsProtocol2` lets a :class:`typing.Generic` subclass or a class with
``__slots__`` pickle under protocol 0 and 1, on GraalPy as on CPython;
:class:`ReduceToName` reduces a singleton to the module-level name holding it;
:class:`ReduceToItems` reduces a multi-valued mapping to a call of its class on its
item pairs; and :func:`reduce_to` makes one instance reduce to a call of a given
Expand Down
104 changes: 79 additions & 25 deletions pcapkit/foundation/traceflow/data/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@
from typing import TYPE_CHECKING

from pcapkit.corekit.infoclass import Info, info_final
from pcapkit.utilities.pickle import ReduceAsProtocol2

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

if TYPE_CHECKING:
from decimal import Decimal
from typing import Any, Optional
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
Expand Down Expand Up @@ -67,7 +66,13 @@ def __call__(self) -> 'tuple[TCP_Datagram, ...]':
return self.reassembly.datagram


class FrameRecord(ReduceAsProtocol2, dict):
#: 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__'}


@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
Expand All @@ -76,44 +81,93 @@ class FrameRecord(ReduceAsProtocol2, dict):
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`).
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`).

__slots__ = ('packet', 'frame_info', 'time_epoch')
"""

#: 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'
#: The dunder keys holding the attributes, which ``repr`` and ``str``
#: leave out as well.
__excluded__ = list(_RECORD.values())

def __init__(self, mapping: 'dict[str, Any]', *, packet: 'bytes',
# 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.packet = packet
self.frame_info = frame_info
self.time_epoch = time_epoch
self.__dict__.update(zip(_RECORD.values(), (packet, frame_info, time_epoch)))

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

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: 'dict[str, Any]', packet: 'bytes',
def _frame_record(mapping: 'Mapping[str, Any]', packet: 'bytes',
frame_info: 'Data_FrameInfo', time_epoch: 'Decimal') -> 'FrameRecord':
"""Rebuild a :class:`FrameRecord` pickled by a release before 1.6.0.
"""Rebuild a pickled :class:`FrameRecord`.

Those pickles call this; a :class:`FrameRecord` reduces through
:class:`~pcapkit.utilities.pickle.ReduceAsProtocol2` instead, which carries
the slots under every protocol (:issue:`2714`).
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.
Expand Down
20 changes: 17 additions & 3 deletions pcapkit/foundation/traceflow/data/tcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from pcapkit.const.reg.linktype import LinkType as Enum_LinkType
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.data import FrameRecord
from pcapkit.protocols.data.misc.pcap.frame import Frame as Data_Frame

_AT = TypeVar('_AT', 'IPv4Address', 'IPv6Address')
Expand Down Expand Up @@ -47,15 +48,28 @@ class Packet(Info, Generic[_AT]):
* :meth:`pcapkit.foundation.traceflow.traceflow.TraceFlow.dump`
* :term:`trace.tcp.packet`

.. versionchanged:: 1.6.0

A third-party engine's :attr:`frame` is a
:class:`~pcapkit.foundation.traceflow.data.data.FrameRecord`, which
:meth:`to_dict` exports as an
:class:`~pcapkit.corekit.multidict.OrderedMultiDict`; so
``Packet.from_dict(packet.to_dict())`` holds the frame as an
:class:`~pcapkit.corekit.infoclass.OrderedMultiInfo`, not a
:class:`~pcapkit.foundation.traceflow.data.data.FrameRecord`
(:issue:`2726`).

"""

#: Data link type: the PCAP global header's, or that of the PCAP-NG
#: interface the packet was captured on.
protocol: 'Enum_LinkType'
#: Frame number.
index: 'int'
#: Extracted frame info.
frame: 'Data_Frame | dict[str, Any]'
#: Extracted frame info: the default engine's frame, a third-party
#: engine's :class:`~pcapkit.foundation.traceflow.data.data.FrameRecord`, or
#: the :obj:`dict` PyShark's adapter reports, which carries no frame record.
frame: 'Data_Frame | FrameRecord | dict[str, Any]'
#: TCP synchronise (SYN) flag.
syn: 'bool'
#: TCP finish (FIN) flag.
Expand Down Expand Up @@ -91,7 +105,7 @@ class Packet(Info, Generic[_AT]):
payload: 'bytearray'

if TYPE_CHECKING:
def __init__(self, protocol: 'Enum_LinkType', index: 'int', frame: 'Data_Frame | dict[str, Any]', syn: 'bool', fin: 'bool', rst: 'bool', src: '_AT', dst: '_AT', srcport: 'int', dstport: 'int', timestamp: 'float', seq: 'int', ack: 'int', header: 'bytes', payload: 'bytearray') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long
def __init__(self, protocol: 'Enum_LinkType', index: 'int', frame: 'Data_Frame | FrameRecord | dict[str, Any]', syn: 'bool', fin: 'bool', rst: 'bool', src: '_AT', dst: '_AT', srcport: 'int', dstport: 'int', timestamp: 'float', seq: 'int', ack: 'int', header: 'bytes', payload: 'bytearray') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long


@info_final
Expand Down
4 changes: 2 additions & 2 deletions pcapkit/protocols/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@
'PCAPNG_ISB_FilterAcceptOption', 'PCAPNG_ISB_OSDropOption', 'PCAPNG_ISB_UsrDelivOption',
'PCAPNG_PACK_FlagsOption', 'PCAPNG_PACK_HashOption',
'PCAPNG_NameResolutionRecord', 'PCAPNG_UnknownRecord', 'PCAPNG_EndRecord', 'PCAPNG_IPv4Record', 'PCAPNG_IPv6Record',
'PCAPNG_DSBSecrets', 'PCAPNG_UnknownSecrets', 'PCAPNG_TLSKeyLog', 'PCAPNG_WireGuardKeyLog', 'PCAPNG_ZigBeeNWKKey',
'PCAPNG_ZigBeeAPSKey',
'PCAPNG_DSBSecrets', 'PCAPNG_UnknownSecrets', 'PCAPNG_TLSKeyLogEntries', 'PCAPNG_TLSKeyLog',
'PCAPNG_WireGuardKeyLog', 'PCAPNG_ZigBeeNWKKey', 'PCAPNG_ZigBeeAPSKey',
'PCAPNG_UnknownBlock', 'PCAPNG_SectionHeaderBlock', 'PCAPNG_InterfaceDescriptionBlock',
'PCAPNG_EnhancedPacketBlock', 'PCAPNG_SimplePacketBlock', 'PCAPNG_NameResolutionBlock',
'PCAPNG_InterfaceStatisticsBlock', 'PCAPNG_SystemdJournalExportBlock', 'PCAPNG_DecryptionSecretsBlock',
Expand Down
5 changes: 3 additions & 2 deletions pcapkit/protocols/data/misc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
from pcapkit.protocols.data.misc.pcapng import \
SystemdJournalExportBlock as PCAPNG_SystemdJournalExportBlock
from pcapkit.protocols.data.misc.pcapng import TLSKeyLog as PCAPNG_TLSKeyLog
from pcapkit.protocols.data.misc.pcapng import TLSKeyLogEntries as PCAPNG_TLSKeyLogEntries
from pcapkit.protocols.data.misc.pcapng import UnknownBlock as PCAPNG_UnknownBlock
from pcapkit.protocols.data.misc.pcapng import UnknownOption as PCAPNG_UnknownOption
from pcapkit.protocols.data.misc.pcapng import UnknownRecord as PCAPNG_UnknownRecord
Expand Down Expand Up @@ -98,8 +99,8 @@
'PCAPNG_ISB_FilterAcceptOption', 'PCAPNG_ISB_OSDropOption', 'PCAPNG_ISB_UsrDelivOption',
'PCAPNG_PACK_FlagsOption', 'PCAPNG_PACK_HashOption',
'PCAPNG_NameResolutionRecord', 'PCAPNG_UnknownRecord', 'PCAPNG_EndRecord', 'PCAPNG_IPv4Record', 'PCAPNG_IPv6Record',
'PCAPNG_DSBSecrets', 'PCAPNG_UnknownSecrets', 'PCAPNG_TLSKeyLog', 'PCAPNG_WireGuardKeyLog', 'PCAPNG_ZigBeeNWKKey',
'PCAPNG_ZigBeeAPSKey',
'PCAPNG_DSBSecrets', 'PCAPNG_UnknownSecrets', 'PCAPNG_TLSKeyLogEntries', 'PCAPNG_TLSKeyLog',
'PCAPNG_WireGuardKeyLog', 'PCAPNG_ZigBeeNWKKey', 'PCAPNG_ZigBeeAPSKey',
'PCAPNG_UnknownBlock', 'PCAPNG_SectionHeaderBlock', 'PCAPNG_InterfaceDescriptionBlock',
'PCAPNG_EnhancedPacketBlock', 'PCAPNG_SimplePacketBlock', 'PCAPNG_NameResolutionBlock',
'PCAPNG_InterfaceStatisticsBlock', 'PCAPNG_SystemdJournalExportBlock', 'PCAPNG_DecryptionSecretsBlock',
Expand Down
79 changes: 74 additions & 5 deletions pcapkit/protocols/data/misc/pcapng.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
"""data models for PCAP-NG file format"""

import collections.abc
import datetime
import decimal
from typing import TYPE_CHECKING
Expand Down Expand Up @@ -29,8 +30,8 @@

'NameResolutionRecord', 'UnknownRecord', 'EndRecord', 'IPv4Record', 'IPv6Record',

'DSBSecrets', 'UnknownSecrets', 'TLSKeyLog', 'WireGuardKeyLog', 'ZigBeeNWKKey',
'ZigBeeAPSKey',
'DSBSecrets', 'UnknownSecrets', 'TLSKeyLogEntries', 'TLSKeyLog', 'WireGuardKeyLog',
'ZigBeeNWKKey', 'ZigBeeAPSKey',

'UnknownBlock', 'SectionHeaderBlock', 'InterfaceDescriptionBlock',
'EnhancedPacketBlock', 'SimplePacketBlock', 'NameResolutionBlock',
Expand All @@ -44,6 +45,7 @@
from datetime import timezone as dt_timezone
from decimal import Decimal
from ipaddress import IPv4Address, IPv4Interface, IPv6Address, IPv6Interface
from typing import Any, Mapping, Optional

from typing_extensions import Literal

Expand Down Expand Up @@ -829,19 +831,86 @@ class UnknownSecrets(DSBSecrets):
def __init__(self, data: 'bytes') -> 'None': ...


@info_final
class TLSKeyLogEntries(Data):
"""Data model for the entries of a TLS key log, keyed by label.

A key is each :class:`~pcapkit.const.pcapng.tls_key_label.TLSKeyLabel`
the log has an entry under, and its value that label's client random to
secret pairs, in log order, as an
:class:`~pcapkit.corekit.infoclass.OrderedMultiInfo`. A label also reads
as an attribute, as ``entries.CLIENT_RANDOM``.

.. versionadded:: 1.6.0

"""

if TYPE_CHECKING:
def __init__(self, dict_: 'Optional[Mapping[Enum_TLSKeyLabel, OrderedMultiDict[bytes, bytes]]]' = None) -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long


@info_final
class TLSKeyLog(DSBSecrets):
"""Data model for TLS key log DSB secrets."""
"""Data model for TLS key log DSB secrets.

.. versionchanged:: 1.6.0

:attr:`entries` is a :class:`TLSKeyLogEntries` rather than a
:obj:`dict`, and a mapping given for it is stored as one; :meth:`to_dict`
exports it as an :class:`~pcapkit.corekit.multidict.OrderedMultiDict`,
as it does any nested data model. A pickle holds it as a :obj:`dict`, so
pickles load across 1.6.0 in both directions (:issue:`2726`).

"""

#: TLS key log entries.
entries: 'dict[Enum_TLSKeyLabel, OrderedMultiInfo[bytes, bytes]]'
entries: 'TLSKeyLogEntries'

if TYPE_CHECKING:
#: TLS key log text as the block carries it, comments and line order
#: included. Set on parsing only, and written back verbatim on rebuild.
data: 'str'

def __init__(self, entries: 'dict[Enum_TLSKeyLabel, OrderedMultiDict[bytes, bytes]]') -> 'None': ...
def __init__(self, entries: 'TLSKeyLogEntries | Mapping[Enum_TLSKeyLabel, OrderedMultiDict[bytes, bytes]]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long

def __post_init__(self) -> 'None':
"""Store a mapping given for :attr:`entries` as a :class:`TLSKeyLogEntries`."""
_hold_entries(self.__dict__)

def __getstate__(self) -> 'dict[str, Any]':
"""Return the state to pickle, holding :attr:`entries` as a :obj:`dict`.

A release before 1.6.0 loads them as the :obj:`dict` it holds, and
:meth:`__setstate__` loads them as a :class:`TLSKeyLogEntries`.

"""
state = dict(self.__dict__)
if isinstance(state.get('entries'), TLSKeyLogEntries):
state['entries'] = dict(state['entries'])
return state

def __setstate__(self, state: 'dict[str, Any]') -> 'None':
"""Restore a pickled state, storing :attr:`entries` as a :class:`TLSKeyLogEntries`.

Args:
state: The state :meth:`__getstate__` returns, or one holding the
entries as a :obj:`dict`, as before 1.6.0.

"""
self.__dict__.update(state)
_hold_entries(self.__dict__)


def _hold_entries(namespace: 'dict[str, Any]') -> 'None':
"""Store the ``entries`` in ``namespace`` as a :class:`TLSKeyLogEntries`, unless they are one.

Args:
namespace: The :attr:`~object.__dict__` of a :class:`TLSKeyLog`.

"""
entries = namespace.get('entries')
if isinstance(entries, collections.abc.Mapping) and not isinstance(entries, TLSKeyLogEntries):
namespace['entries'] = TLSKeyLogEntries(entries)


@info_final
Expand Down
Loading
Loading