Skip to content

Commit d4d6655

Browse files
Fix cache issues with packet list fields (#4705)
- Add `list_field` class with `list_field_meta`. - `list_field.ensure_bound()` calls added anywhere `Packet.fields` is updated with a list field.
1 parent 7f0f095 commit d4d6655

File tree

1 file changed

+118
-6
lines changed

1 file changed

+118
-6
lines changed

scapy/packet.py

Lines changed: 118 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -203,16 +203,28 @@ def __init__(self,
203203
value = fields.pop(fname)
204204
except KeyError:
205205
continue
206-
self.fields[fname] = value if isinstance(value, RawVal) else \
207-
self.get_field(fname).any2i(self, value)
206+
if not isinstance(value, RawVal):
207+
value = self.get_field(fname).any2i(self, value)
208+
209+
# In case of a list, ensure we store a `list_field` instance, not a simple `list`.
210+
if isinstance(value, list):
211+
value = list_field.ensure_bound(self, value)
212+
213+
self.fields[fname] = value
208214
# The remaining fields are unknown
209215
for fname in fields:
210216
if fname in self.deprecated_fields:
211217
# Resolve deprecated fields
212218
value = fields[fname]
213219
fname = self._resolve_alias(fname)
214-
self.fields[fname] = value if isinstance(value, RawVal) else \
215-
self.get_field(fname).any2i(self, value)
220+
if not isinstance(value, RawVal):
221+
value = self.get_field(fname).any2i(self, value)
222+
223+
# In case of a list, ensure we store a `list_field` instance, not a simple `list`.
224+
if isinstance(value, list):
225+
value = list_field.ensure_bound(self, value)
226+
227+
self.fields[fname] = value
216228
continue
217229
raise AttributeError(fname)
218230
if isinstance(post_transform, list):
@@ -320,6 +332,11 @@ def do_init_cached_fields(self, for_dissect_only=False, init_fields=None):
320332

321333
# Fix: Use `copy_field_value()` instead of just `value.copy()`, in order to duplicate list items as well in case of a list.
322334
self.fields[fname] = self.copy_field_value(fname, self.default_fields[fname])
335+
336+
# In case of a list, ensure we store a `list_field` instance, not a simple `list`.
337+
if isinstance(self.fields[fname], list):
338+
self.fields[fname] = list_field.ensure_bound(self, self.fields[fname])
339+
323340
self._ensure_parent_of(self.fields[fname])
324341

325342
def prepare_cached_fields(self, flist):
@@ -663,8 +680,14 @@ def setfieldval(self, attr, val):
663680
any2i = lambda x, y: y # type: Callable[..., Any]
664681
else:
665682
any2i = fld.any2i
666-
self.fields[attr] = val if isinstance(val, RawVal) else \
667-
any2i(self, val)
683+
if not isinstance(val, RawVal):
684+
val = any2i(self, val)
685+
686+
# In case of a list, ensure we store a `list_field` instance, not a simple `list`.
687+
if isinstance(val, list):
688+
val = list_field.ensure_bound(self, val)
689+
690+
self.fields[attr] = val
668691
self.explicit = 0
669692
# Invalidate cache when the packet has changed.
670693
self.clear_cache(upwards=True, downwards=False)
@@ -1210,6 +1233,11 @@ def do_dissect(self, s):
12101233
# Skip unused ConditionalField
12111234
if isinstance(f, ConditionalField) and fval is None:
12121235
continue
1236+
1237+
# In case of a list, ensure we store a `list_field` instance, not a simple `list`.
1238+
if isinstance(fval, list):
1239+
fval = list_field.ensure_bound(self, fval)
1240+
12131241
self.fields[f.name] = fval
12141242
# Nothing left to dissect
12151243
if not s and (isinstance(f, MayEnd) or
@@ -2133,6 +2161,90 @@ def route(self):
21332161
return (None, None, None)
21342162

21352163

2164+
#################
2165+
# list fields #
2166+
#################
2167+
2168+
2169+
class list_field_meta(type):
2170+
"""
2171+
Wraps modifying methods for ``list`` base type.
2172+
2173+
Inspired from https://stackoverflow.com/questions/8858525/track-changes-to-lists-and-dictionaries-in-python#8859168.
2174+
"""
2175+
def __new__(
2176+
mcs,
2177+
name, # type: str
2178+
bases, # Tuple[type, ...]
2179+
attrs, # type: Dict[str, Any]
2180+
): # type: (...) -> type
2181+
# List names of `list` methods modifying the list.
2182+
for method_name in [
2183+
"append",
2184+
"clear",
2185+
"extend",
2186+
"insert",
2187+
"pop",
2188+
"remove",
2189+
"reverse", # Memo: Reverse *IN PLACE*.
2190+
"sort", # Memo: Stable sort *IN PLACE*.
2191+
"__delitem__",
2192+
"__iadd__",
2193+
"__imul__",
2194+
"__setitem__",
2195+
]:
2196+
# Wrap the method so that `Packet.clear_cache()` be automatically called.
2197+
attrs[method_name] = list_field_meta._wrap_method(getattr(list, method_name))
2198+
return type.__new__(mcs, name, bases, attrs)
2199+
2200+
@staticmethod
2201+
def _wrap_method(meth): # type: (Callable[[Any, ...], Any]) -> Callable[[Any, ...], Any]
2202+
def wrapped(
2203+
self, # type: list_field
2204+
*args, # type: Any
2205+
**kwargs, # type: Any
2206+
): # type: (...) -> Any
2207+
# Automatically call `Packet.clear_cache()` when the `list_field` is modified.
2208+
self.pkt.clear_cache(upwards=True, downwards=False)
2209+
2210+
# Call the wrapped method, and return its result.
2211+
return meth(self, *args, **kwargs)
2212+
return wrapped
2213+
2214+
2215+
class list_field(list, metaclass=list_field_meta):
2216+
"""
2217+
Overrides the base ``list`` type for list fields bound with packets.
2218+
2219+
Ensures :meth:`Packet.clear_cache()` is called when the list is modified.
2220+
2221+
Lower case for the class name in order to avoid confusions with classes like ``PacketListField``.
2222+
"""
2223+
def __init__(
2224+
self,
2225+
pkt, # type: Packet,
2226+
*args # type: Any
2227+
): # type: (...) -> None
2228+
# Call the `list.__init__()` super constructor.
2229+
super().__init__(*args)
2230+
2231+
#: Packet bound with this list field.
2232+
self.pkt = pkt
2233+
2234+
@staticmethod
2235+
def ensure_bound(
2236+
pkt, # type: Packet
2237+
lst, # type: List[Any]
2238+
): # type: (...) -> list_field
2239+
"""
2240+
Ensures a :class:`list_field` instance bound with ``pkt``.
2241+
"""
2242+
# If `lst` is a simple `list`, this method intends to create a new `list_field` instance.
2243+
# If `lst` is already a `list_field` instance, we never know where this instance comes from, and what it's being used for.
2244+
# Let's create a new `list_field` instance in any case.
2245+
return list_field(pkt, lst)
2246+
2247+
21362248
####################
21372249
# packet classes #
21382250
####################

0 commit comments

Comments
 (0)