@@ -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