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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ Preceded by `1.5.0a1` (2026-09-15), `1.5.0b1` to `1.5.0b3` (all 2026-09-19) and
- the number registrars `register_tcp`, `register_udp`, `register_sctp` and `register_apptype` refuse a `bool` code with `RegistryError`, where `register_tcp(True, ...)` registered port 1, since `bool` is an `int` subclass ([#2269](https://github.com/JarryShaw/PyPCAPKit/issues/2269), [#2344](https://github.com/JarryShaw/PyPCAPKit/pull/2344)).
- IP reassembly reports a datagram that is not itself fragmented but whose BUFID is already buffered: it submits that buffer as incomplete and then reassembles the datagram in a buffer of its own, per [RFC 791 Section 3.2](https://datatracker.ietf.org/doc/html/rfc791#section-3.2), where the datagram was never reported at all. On IPv6 an atomic fragment is reassembled in a buffer of its own and submitted without flushing the datagram already being reassembled, so that one's final fragment still completes it, per [RFC 6946 Section 4](https://datatracker.ietf.org/doc/html/rfc6946#section-4) ([#2270](https://github.com/JarryShaw/PyPCAPKit/issues/2270), [#2345](https://github.com/JarryShaw/PyPCAPKit/pull/2345)).
- the PyPCAP, pcap-ct and pypcapfile engines warn with `AttributeWarning`, naming the engine, that they ignore a parsing context supplied through `Extractor`'s `context=`, as the dpkt, scapy and pyshark engines already did, where they dropped it silently ([#2313](https://github.com/JarryShaw/PyPCAPKit/issues/2313), [#2332](https://github.com/JarryShaw/PyPCAPKit/pull/2332)).
- `Deferred` could not be pickled under protocol 0 or 1 -- a slotted class with no `__getstate__` raises `TypeError` there, independent of the `analyze`/`proto` regression [#2417](https://github.com/JarryShaw/PyPCAPKit/issues/2417) fixed. It now defines `__getstate__` and `__setstate__`, mirroring the `(dict_or_None, slots)` pair Python's own default slotted-class reduction already used for every released build, so a `Deferred` pickled by one of those still unpickles. The slot names come from `copyreg._slotnames` rather than a hand-rolled walk of `type(self).__mro__`, which would otherwise shadow a base class's slots under a subclass declaring its own, miscount a bare-string `__slots__ = 'name'`, or read a private, name-mangled slot under its unmangled spelling ([#2423](https://github.com/JarryShaw/PyPCAPKit/issues/2423), [#2425](https://github.com/JarryShaw/PyPCAPKit/issues/2425), [#2424](https://github.com/JarryShaw/PyPCAPKit/pull/2424)).

### pcapkit.interface

Expand Down
11 changes: 11 additions & 0 deletions docs/source/changelog/1.5.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1262,6 +1262,17 @@ Fixed
naming the engine, that they ignore a parsing context supplied through
:class:`.Extractor`'s ``context=``, as the dpkt, scapy and pyshark engines already
did, where they dropped it silently (:issue:`2313`, :pr:`2332`).
* :class:`~pcapkit.foundation.reassembly.data.data.Deferred` could not be pickled
under protocol 0 or 1 -- a slotted class with no ``__getstate__`` raises
:exc:`TypeError` there, independent of the ``analyze``/``proto`` regression
:issue:`2417` fixed. It now defines ``__getstate__`` and ``__setstate__``,
mirroring the ``(dict_or_None, slots)`` pair Python's own default slotted-class
reduction already used for every released build, so a ``Deferred`` pickled by
one of those still unpickles. The slot names come from ``copyreg._slotnames``
rather than a hand-rolled walk of ``type(self).__mro__``, which would otherwise
shadow a base class's slots under a subclass declaring its own, miscount a
bare-string ``__slots__ = 'name'``, or read a private, name-mangled slot under
its unmangled spelling (:issue:`2423`, :issue:`2425`, :pr:`2424`).

pcapkit.interface
-----------------
Expand Down
66 changes: 65 additions & 1 deletion pcapkit/foundation/reassembly/data/data.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
"""shared data models for reassembly"""

import copyreg
from typing import TYPE_CHECKING, Generic, TypeVar

from pcapkit.corekit.enum import EnumLookup
Expand All @@ -10,7 +11,7 @@
__all__ = ['ReassemblyData', 'Completion', 'Deferred']

if TYPE_CHECKING:
from typing import Callable, Optional
from typing import Any, Callable, Optional

from pcapkit.const.reg.transtype import TransType
from pcapkit.foundation.reassembly.data.ip import Datagram as IP_Datagram
Expand Down Expand Up @@ -206,6 +207,69 @@ def __call__(self) -> 'ProtocolBase':
"""
return self.analyze(self.proto, self.payload)

def __getstate__(self) -> 'tuple[Optional[dict[str, Any]], dict[str, Any]]':
"""Pickling support.

A slotted class with no ``__dict__`` has nothing for the default
:mod:`pickle` reduction to read. :mod:`copyreg` refuses outright with
*"cannot be pickled"* under protocol 0 or 1 whenever
``type(self).__getstate__`` is still the inherited default -- which is
why defining *any* ``__getstate__`` here, even one that only calls the
default, clears protocols 0 and 1 (:issue:`2423`). Protocol 2 and above
never needed one: their own ``__reduce_ex__`` already walks
``__slots__`` directly.

The shape returned -- an instance-``__dict__`` state (or :data:`None`)
paired with a slots-state :obj:`dict` -- is deliberately the same one
Python's own default state construction produces for a slotted class,
rather than a bespoke flat :obj:`dict`. A flat shape would be a *new*
pickle format for this class: a ``Deferred`` already pickled by a
released version under protocol 2 or above -- every one of them built
this same ``(dict_or_None, slots)`` pair -- would fail to unpickle
against a flat-shaped :meth:`__setstate__`, and the reverse.

The slot names come from :func:`copyreg._slotnames`, the standard
library's own helper for this exact job, rather than a hand-rolled
walk of ``type(self).__mro__`` and each class's ``__slots__``: a
subclass of ``Deferred`` may spell a slot as a bare string
(``__slots__ = 'extra'``, valid Python for one slot named by the
whole string) or as a private, name-mangled one
(``__slots__ = ('__secret',)``, stored on the instance under
``_<ClassName>__secret``), and a hand-rolled walk that assumes every
``__slots__`` is an iterable of already-resolved attribute names gets
both wrong -- the first by iterating the string's *characters*, the
second by testing the unmangled name ``hasattr`` never finds
(:issue:`2425`). ``_slotnames`` already handles both, the same way
protocol 0 and 1 reduction has relied on it to for decades.

"""
slotnames = copyreg._slotnames(type(self)) # type: ignore[attr-defined] # pylint: disable=protected-access
slots = {name: getattr(self, name) for name in slotnames if hasattr(self, name)}
dict_state = dict(self.__dict__) if getattr(self, '__dict__', None) else None
return (dict_state, slots)

def __setstate__(
self,
state: 'tuple[Optional[dict[str, Any]], dict[str, Any]] | dict[str, Any]',
) -> 'None':
"""Unpickling support; the counterpart to :meth:`__getstate__`.

Accepts the ``(dict_or_None, slots)`` pair :meth:`__getstate__` builds
above -- the shape every released, pre-:issue:`2423` pickle already
used under protocol 2 and above -- and, for a pickle made by this
class's own first, unreleased fix for :issue:`2423`, a bare :obj:`dict`
of ``{slot: value}`` (:issue:`2425`).

"""
if isinstance(state, dict):
dict_state, slots = None, state
else:
dict_state, slots = state
if dict_state:
self.__dict__.update(dict_state)
for slot, value in slots.items():
setattr(self, slot, value)


@info_final
class ReassemblyData(Info):
Expand Down
Loading
Loading