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
113 changes: 64 additions & 49 deletions scapy/packet.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,16 @@ def __init__(self,
else:
self.post_transforms = [post_transform]

_PickleType = Tuple[
Union[EDecimal, float],
Optional[Union[EDecimal, float, None]],
Optional[int],
Optional[_GlobInterfaceType],
Optional[int],
Optional[bytes],
]
_PickleStateType = Union[_PickleType, Dict[str, Any]]

@property
def comment(self):
# type: () -> Optional[bytes]
Expand All @@ -235,63 +245,68 @@ def comment(self, value):
self.comments = None

@classmethod
def _rebuild_pkt(
cls, # type: Type[Packet]
fields, # type: Dict[str, Any]
payload, # type: Optional[Packet]
metadata, # type: Dict[str, Any]
extra_slots={}, # type: Dict[str, Any]
):
# type: (...) -> Packet
"""Helper for unpickling Packet instances via field values."""
# Create the instance using the field values
pkt = cls(**fields)
if payload is not None:
pkt.add_payload(payload)
# Restore metadata
pkt.time = metadata['time']
pkt.sent_time = metadata['sent_time']
pkt.direction = metadata['direction']
pkt.sniffed_on = metadata['sniffed_on']
pkt.wirelen = metadata['wirelen']
pkt.comments = metadata['comments']
# Restore any extra __slots__ defined by subclasses
for attr, value in extra_slots.items():
setattr(pkt, attr, value)
return pkt
def _rebuild_pkt(cls, raw_packet):
# type: (Type[Packet], bytes) -> Packet
"""Helper used by pickle to reconstruct Packet from raw bytes."""
return cls(raw_packet)

def __reduce__(self):
# type: () -> Tuple[Any, ...]
"""Used by pickling methods.

Reconstructs the packet from field values, payload, and metadata.
"""
# Store field values for unpickling
fields = {}
for f in self.fields_desc:
if f.name in self.fields:
fields[f.name] = self.fields[f.name]
payload = self.payload # type: Optional[Packet]
if isinstance(payload, NoPayload):
payload = None
# Store metadata for unpickling
metadata = {
'time': self.time,
'sent_time': self.sent_time,
'direction': self.direction,
'sniffed_on': self.sniffed_on,
'wirelen': self.wirelen,
'comments': self.comments,
"""Used by pickling methods"""
state = {
"pickle_state_version": 2,
"time": self.time,
"sent_time": self.sent_time,
"direction": self.direction,
"sniffed_on": self.sniffed_on,
"wirelen": self.wirelen,
# Keep both keys for compatibility with historical/transition code.
"comment": self.comment,
"comments": self.comments,
}
# Collect any extra __slots__ defined by subclasses
extra_slots = {}
for attr in type(self).__all_slots__ - set(Packet.__slots__):
if hasattr(self, attr):
extra_slots[attr] = getattr(self, attr)
return (
type(self)._rebuild_pkt,
(fields, payload, metadata, extra_slots),
)
if extra_slots:
state["extra_slots"] = extra_slots # type: ignore
return (type(self)._rebuild_pkt, (self.build(),), state)

def __setstate__(self, state):
# type: (Packet._PickleStateType) -> Packet
"""Rebuild state using pickable methods"""
# Legacy format: tuple produced by older Packet.__reduce__.
if isinstance(state, tuple):
self.time = state[0]
self.sent_time = state[1]
self.direction = state[2]
self.sniffed_on = state[3]
self.wirelen = state[4]
self.comment = state[5]
return self

# New format: versioned dict metadata.
self.time = state.get("time", self.time)
self.sent_time = state.get("sent_time", self.sent_time)
self.direction = state.get("direction", self.direction)
self.sniffed_on = state.get("sniffed_on", self.sniffed_on)
self.wirelen = state.get("wirelen", self.wirelen)

if "comments" in state:
self.comments = state["comments"]
elif "comment" in state:
self.comment = state["comment"]

extra_slots = state.get("extra_slots", {})
if isinstance(extra_slots, dict):
for attr, value in extra_slots.items():
# Only restore known subclass slots; ignore stale/unknown entries.
if attr in type(self).__all_slots__ and attr not in Packet.__slots__:
try:
setattr(self, attr, value)
except AttributeError:
pass
return self

def __deepcopy__(self,
memo, # type: Any
Expand Down
42 changes: 39 additions & 3 deletions test/regression.uts
Original file line number Diff line number Diff line change
Expand Up @@ -633,9 +633,6 @@ assert p2[IP].dst == '5.6.7.8'
assert p2[TCP].sport == 1234
assert p2[TCP].dport == 80
assert p2[TCP].flags == 'S'
assert p2[IP].len is None
assert p2[IP].chksum is None
assert p2[TCP].chksum is None
assert p2.time == 12345.0
assert p2.sent_time == 12346.0
assert p2.direction == 1
Expand Down Expand Up @@ -4845,6 +4842,45 @@ assert pl[0].wirelen == 1
assert pl[0][Ether].src == '00:11:22:33:44:55'
assert pl[1][Ether].dst == '00:22:33:44:55:66'

= __setstate__ accepts legacy tuple state (pickle backward compatibility)

legacy = Ether(src='00:01:02:03:04:05', dst='00:06:07:08:09:0a')/Raw(b'legacy')
legacy.__setstate__((
EDecimal("42.5"),
EDecimal("43.5"),
1,
"legacy0",
128,
b"legacy-comment",
))
assert legacy.time == 42.5
assert legacy.sent_time == 43.5
assert legacy.direction == 1
assert legacy.sniffed_on == "legacy0"
assert legacy.wirelen == 128
assert legacy.comment == b"legacy-comment"
assert legacy.comments == [b"legacy-comment"]

= pickle roundtrip keeps packet metadata and comments list

meta = Ether(src='10:11:12:13:14:15', dst='20:21:22:23:24:25')/Raw(b'meta')
meta.time = EDecimal("100.25")
meta.sent_time = EDecimal("100.75")
meta.direction = 2
meta.sniffed_on = "meta0"
meta.wirelen = 256
meta.comments = [b"first", b"second"]

meta2 = pickle.loads(pickle.dumps(meta))
assert bytes(meta2) == bytes(meta)
assert meta2.time == 100.25
assert meta2.sent_time == 100.75
assert meta2.direction == 2
assert meta2.sniffed_on == "meta0"
assert meta2.wirelen == 256
assert meta2.comments == [b"first", b"second"]
assert meta2.comment == b"first"

= EDecimal

# GH4488
Expand Down
Loading