Source code for volatility3.framework.objects

# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#

import collections
import collections.abc
import logging
import struct
from typing import (
    Any,
    ClassVar,
    Dict,
    Iterable,
    List,
    Optional,
    Tuple,
    Type,
    Union as TUnion,
    overload,
)

from volatility3.framework import constants, interfaces
from volatility3.framework.objects import templates

vollog = logging.getLogger(__name__)

DataFormatInfo = collections.namedtuple(
    "DataFormatInfo", ["length", "byteorder", "signed"]
)


[docs] def convert_data_to_value( data: bytes, struct_type: Type[TUnion[int, float, bytes, str, bool]], data_format: DataFormatInfo, ) -> TUnion[int, float, bytes, str, bool]: """Converts a series of bytes to a particular type of value.""" if struct_type is int: return int.from_bytes( data, byteorder=data_format.byteorder, signed=data_format.signed ) if struct_type is bool: struct_format = "?" elif struct_type is float: float_vals = "zzezfzzzd" if ( data_format.length > len(float_vals) or float_vals[data_format.length] not in "efd" ): raise ValueError("Invalid float size") struct_format = ( "<" if data_format.byteorder == "little" else ">" ) + float_vals[data_format.length] elif struct_type in [bytes, str]: struct_format = str(data_format.length) + "s" else: raise TypeError(f"Cannot construct struct format for type {type(struct_type)}") return struct.unpack(struct_format, data)[0]
[docs] def convert_value_to_data( value: TUnion[int, float, bytes, str, bool], struct_type: Type[TUnion[int, float, bytes, str, bool]], data_format: DataFormatInfo, ) -> bytes: """Converts a particular value to a series of bytes.""" if not isinstance(value, struct_type): raise TypeError( f"Written value is not of the correct type for {struct_type.__name__}" ) if struct_type is int and isinstance(value, int): # Doubling up on the isinstance is for mypy return int.to_bytes( value, length=data_format.length, byteorder=data_format.byteorder, signed=data_format.signed, ) if struct_type is bool: struct_format = "?" elif struct_type is float: float_vals = "zzezfzzzd" if ( data_format.length > len(float_vals) or float_vals[data_format.length] not in "efd" ): raise ValueError("Invalid float size") struct_format = ( "<" if data_format.byteorder == "little" else ">" ) + float_vals[data_format.length] elif struct_type in [bytes, str]: if isinstance(value, str): value = bytes(value, "latin-1") struct_format = str(data_format.length) + "s" else: raise TypeError(f"Cannot construct struct format for type {type(struct_type)}") return struct.pack(struct_format, value)
[docs] class Void(interfaces.objects.ObjectInterface): """Returns an object to represent void/unknown types."""
[docs] class VolTemplateProxy(interfaces.objects.ObjectInterface.VolTemplateProxy):
[docs] @classmethod def size(cls, template: interfaces.objects.Template) -> int: """Dummy size for Void objects. According to http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf, void is an incomplete type, and therefore sizeof(void) should fail. However, we need to be able to construct voids to be able to cast them, so we return a useless size. It shouldn't cause errors, but it also shouldn't be common, it is logged at the lowest level. """ vollog.log(constants.LOGLEVEL_VVVV, "Void size requested") return 0
[docs] def write(self, value: Any) -> None: """Dummy method that does nothing for Void objects.""" raise TypeError("Cannot write data to a void, recast as another object")
[docs] class Function(interfaces.objects.ObjectInterface): """"""
[docs] class PrimitiveObject(interfaces.objects.ObjectInterface): """PrimitiveObject is an interface for any objects that should simulate a Python primitive.""" _struct_type: ClassVar[Type] = int def __init__( self, context: interfaces.context.ContextInterface, type_name: str, object_info: interfaces.objects.ObjectInformation, data_format: DataFormatInfo, ) -> None: super().__init__( context=context, type_name=type_name, object_info=object_info, data_format=data_format, ) self._data_format = data_format def __new__( cls: Type, context: interfaces.context.ContextInterface, type_name: str, object_info: interfaces.objects.ObjectInformation, data_format: DataFormatInfo, new_value: Optional[TUnion[int, float, bool, bytes, str]] = None, **kwargs, ) -> "PrimitiveObject": """Creates the appropriate class and returns it so that the native type is inherited. The only reason the kwargs is added, is so that the inheriting types can override __init__ without needing to override __new__ We also sneak in new_value, so that we don't have to do expensive (read: impossible) context reads when unpickling. """ if new_value is None: value = cls._unmarshall(context, data_format, object_info) else: value = new_value result = cls._struct_type.__new__(cls, value) # This prevents us having to go read a context layer when recreating after unpickling # Mypy complains that result doesn't have a __new_value, but using setattr causes pycharm to complain further down result.__new_value = value # type: ignore return result def __getnewargs_ex__(self): """Make sure that when pickling, all appropriate parameters for new are provided.""" kwargs = {} for k, v in self._vol.maps[-1].items(): if k not in ["context", "data_format", "object_info", "type_name"]: kwargs[k] = v kwargs["new_value"] = self.__new_value return ( self._context, self._vol.maps[-3]["type_name"], self._vol.maps[-2], self._data_format, ), kwargs @classmethod def _unmarshall( cls, context: interfaces.context.ContextInterface, data_format: DataFormatInfo, object_info: interfaces.objects.ObjectInformation, ) -> TUnion[int, float, bool, bytes, str]: # Don't try to lookup a 0 length data format, incase it's at an invalid offset. Length 0 means b'' data = b"" if data_format.length > 0: data = context.layers.read( object_info.layer_name, object_info.offset, data_format.length ) return convert_data_to_value(data, cls._struct_type, data_format)
[docs] class VolTemplateProxy(interfaces.objects.ObjectInterface.VolTemplateProxy):
[docs] @classmethod def size(cls, template: interfaces.objects.Template) -> int: """Returns the size of the templated object.""" return template.vol.data_format.length
[docs] def write( self, value: TUnion[int, float, bool, bytes, str] ) -> interfaces.objects.ObjectInterface: """Writes the object into the layer of the context at the current offset.""" data = convert_value_to_data(value, self._struct_type, self._data_format) self._context.layers.write(self.vol.layer_name, self.vol.offset, data) return self.cast(self.vol.type_name)
# This must be int (and the _struct_type must be int) because bool cannot be inherited from: # https://mail.python.org/pipermail/python-dev/2002-March/020822.html # https://mail.python.org/pipermail/python-dev/2004-February/042537.html
[docs] class Boolean(PrimitiveObject, int): """Primitive Object that handles boolean types.""" _struct_type: ClassVar[Type] = int
[docs] class Integer(PrimitiveObject, int): """Primitive Object that handles standard numeric types."""
[docs] class Float(PrimitiveObject, float): """Primitive Object that handles double or floating point numbers.""" _struct_type: ClassVar[Type] = float
[docs] class Char(PrimitiveObject, int): """Primitive Object that handles characters.""" _struct_type: ClassVar[Type] = int
[docs] class Bytes(PrimitiveObject, bytes): """Primitive Object that handles specific series of bytes.""" _struct_type: ClassVar[Type] = bytes def __init__( self, context: interfaces.context.ContextInterface, type_name: str, object_info: interfaces.objects.ObjectInformation, length: int = 1, ) -> None: super().__init__( context=context, type_name=type_name, object_info=object_info, data_format=DataFormatInfo(length, "big", False), ) self._vol["length"] = length def __new__( cls: Type, context: interfaces.context.ContextInterface, type_name: str, object_info: interfaces.objects.ObjectInformation, length: int = 1, **kwargs, ) -> "Bytes": """Creates the appropriate class and returns it so that the native type is inherited. The only reason the kwargs is added, is so that the inheriting types can override __init__ without needing to override __new__ """ return cls._struct_type.__new__( cls, cls._unmarshall( context, data_format=DataFormatInfo(length, "big", False), object_info=object_info, ), )
[docs] class VolTemplateProxy(interfaces.objects.ObjectInterface.VolTemplateProxy):
[docs] @classmethod def size(cls, template: interfaces.objects.Template) -> int: return template.vol.length
[docs] class String(PrimitiveObject, str): """Primitive Object that handles string values. Args: max_length: specifies the maximum possible length that the string could hold within memory (for multibyte characters, this will not be the maximum length of the string) """ _struct_type: ClassVar[Type] = str def __init__( self, context: interfaces.context.ContextInterface, type_name: str, object_info: interfaces.objects.ObjectInformation, max_length: int = 1, encoding: str = "utf-8", errors: str = "strict", ) -> None: super().__init__( context=context, type_name=type_name, object_info=object_info, data_format=DataFormatInfo(max_length, "big", False), ) self._vol["max_length"] = max_length self._vol["encoding"] = encoding self._vol["errors"] = errors def __new__( cls: Type, context: interfaces.context.ContextInterface, type_name: str, object_info: interfaces.objects.ObjectInformation, max_length: int = 1, encoding: str = "utf-8", errors: str = "strict", **kwargs, ) -> "String": """Creates the appropriate class and returns it so that the native type is inherited. The only reason the kwargs is added, is so that the inheriting types can override __init__ without needing to override __new__ """ params = {} if encoding: params["encoding"] = encoding if errors: params["errors"] = errors # Pass the encoding and error parameters to the string constructor to appropriately encode the string value = cls._struct_type.__new__( cls, cls._unmarshall( context, data_format=DataFormatInfo(max_length, "big", False), object_info=object_info, ), **params, ) index = value.find("\x00") if index >= 0: value = value[:index] return value
[docs] class VolTemplateProxy(interfaces.objects.ObjectInterface.VolTemplateProxy):
[docs] @classmethod def size(cls, template: interfaces.objects.Template) -> int: """Returns the size of the templated object.""" return template.vol.max_length
[docs] class Pointer(Integer): """Pointer which points to another object.""" def __init__( self, context: interfaces.context.ContextInterface, type_name: str, object_info: interfaces.objects.ObjectInformation, data_format: DataFormatInfo, subtype: Optional[templates.ObjectTemplate] = None, ) -> None: super().__init__( context=context, object_info=object_info, type_name=type_name, data_format=data_format, ) self._vol["subtype"] = subtype self._cache: Dict[str, interfaces.objects.ObjectInterface] = {} @classmethod def _unmarshall( cls, context: interfaces.context.ContextInterface, data_format: DataFormatInfo, object_info: interfaces.objects.ObjectInformation, ) -> Any: """Ensure that pointer values always fall within the domain of the layer they're constructed on. If there's a need for all the data within the address, the pointer should be recast. The "pointer" must always live within the space (even if the data provided is invalid). """ mask = context.layers[object_info.native_layer_name].address_mask new = ( cls._get_raw_value( context, data_format, object_info.layer_name, object_info.offset ) & mask ) return new @classmethod def _get_raw_value( cls, context: interfaces.context.ContextInterface, data_format: DataFormatInfo, layer_name: str, offset: int, ) -> int: length, endian, signed = data_format if signed: raise ValueError("Pointers cannot have signed values") data = context.layers.read(layer_name, offset, length) value = int.from_bytes(data, byteorder=endian, signed=signed) return value
[docs] def get_raw_value(self) -> int: raw = self._get_raw_value( self._context, self.vol.data_format, self.vol.layer_name, self.vol.offset ) return raw
[docs] def dereference( self, layer_name: Optional[str] = None ) -> interfaces.objects.ObjectInterface: """Dereferences the pointer. Layer_name is identifies the appropriate layer within the context that the pointer points to. If layer_name is None, it defaults to the same layer that the pointer is currently instantiated in. """ # Do our own caching because lru_cache doesn't seem to memoize correctly across multiple uses # Cache clearing should be done by a cast (we can add a specific method to reset a pointer, # but hopefully it's not necessary) if layer_name is None: layer_name = self.vol.native_layer_name if self._cache.get(layer_name, None) is None: layer_name = layer_name or self.vol.native_layer_name mask = self._context.layers[layer_name].address_mask offset = self & mask self._cache[layer_name] = self.vol.subtype( context=self._context, object_info=interfaces.objects.ObjectInformation( layer_name=layer_name, offset=offset, parent=self, size=self.vol.subtype.size, native_layer_name=layer_name, ), ) return self._cache[layer_name]
[docs] def is_readable(self, layer_name: Optional[str] = None) -> bool: """Determines whether the address of this pointer can be read from memory.""" layer_name = layer_name or self.vol.native_layer_name return self._context.layers[layer_name].is_valid(self, self.vol.subtype.size)
def __getattr__(self, attr: str) -> Any: """Convenience function to access unknown attributes by getting them from the subtype object.""" if attr in ["vol", "_vol", "_cache"]: raise AttributeError("Pointer not initialized before use") return getattr(self.dereference(), attr)
[docs] def has_member(self, member_name: str) -> bool: """Returns whether the dereferenced type has this member.""" return self._vol["subtype"].has_member(member_name)
[docs] class VolTemplateProxy(interfaces.objects.ObjectInterface.VolTemplateProxy):
[docs] @classmethod def size(cls, template: interfaces.objects.Template) -> int: return Integer.VolTemplateProxy.size(template)
[docs] @classmethod def children( cls, template: interfaces.objects.Template ) -> List[interfaces.objects.Template]: """Returns the children of the template.""" if "subtype" in template.vol: return [template.vol.subtype] return []
[docs] @classmethod def replace_child( cls, template: interfaces.objects.Template, old_child: interfaces.objects.Template, new_child: interfaces.objects.Template, ) -> None: """Substitutes the old_child for the new_child.""" if "subtype" in template.vol: if template.vol.subtype == old_child: template.update_vol(subtype=new_child)
[docs] @classmethod def has_member( cls, template: interfaces.objects.Template, member_name: str ) -> bool: return template.vol["subtype"].has_member(member_name)
[docs] class BitField(interfaces.objects.ObjectInterface, int): """Object containing a field which is made up of bits rather than whole bytes.""" def __init__( self, context: interfaces.context.ContextInterface, type_name: str, object_info: interfaces.objects.ObjectInformation, base_type: interfaces.objects.Template, start_bit: int = 0, end_bit: int = 0, ) -> None: super().__init__(context, type_name, object_info) self._vol["base_type"] = base_type self._vol["start_bit"] = start_bit self._vol["end_bit"] = end_bit def __new__( cls, context: interfaces.context.ContextInterface, type_name: str, object_info: interfaces.objects.ObjectInformation, base_type: interfaces.objects.Template, start_bit: int = 0, end_bit: int = 0, **kwargs, ) -> "BitField": value = base_type(context=context, object_info=object_info) return int.__new__(cls, ((value & ((1 << end_bit) - 1)) >> start_bit)) # type: ignore
[docs] def write(self, value): raise NotImplementedError("Writing to BitFields is not yet implemented")
[docs] class VolTemplateProxy(interfaces.objects.ObjectInterface.VolTemplateProxy):
[docs] @classmethod def size(cls, template: interfaces.objects.Template) -> int: return template.vol.base_type.size
[docs] @classmethod def children( cls, template: interfaces.objects.Template ) -> List[interfaces.objects.Template]: """Returns the children of the template.""" if "base_type" in template.vol: return [template.vol.base_type] return []
[docs] @classmethod def replace_child( cls, template: interfaces.objects.Template, old_child: interfaces.objects.Template, new_child: interfaces.objects.Template, ) -> None: """Substitutes the old_child for the new_child.""" if "base_type" in template.vol: if template.vol.base_type == old_child: template.update_vol(base_type=new_child)
[docs] class Enumeration(interfaces.objects.ObjectInterface, int): """Returns an object made up of choices.""" def __new__( cls, context: interfaces.context.ContextInterface, type_name: str, object_info: interfaces.objects.ObjectInformation, base_type: interfaces.objects.Template, choices: Dict[str, int], **kwargs, ) -> "Enumeration": value = base_type(context=context, object_info=object_info) return int.__new__(cls, value) # type: ignore def __init__( self, context: interfaces.context.ContextInterface, type_name: str, object_info: interfaces.objects.ObjectInformation, base_type: Integer, choices: Dict[str, int], ) -> None: super().__init__(context, type_name, object_info) self._inverse_choices = self._generate_inverse_choices(choices) self._vol["choices"] = choices self._vol["base_type"] = base_type def __eq__(self, other): """An enumeration must be equivalent to its value, even if the other value is not an enumeration""" return int(self) == other def __hash__(self): """Enumerations must be hashed as equivalent to their integer counterparts""" return super().__hash__() @classmethod def _generate_inverse_choices(cls, choices: Dict[str, int]) -> Dict[int, str]: """Generates the inverse choices for the object.""" inverse_choices: Dict[int, str] = {} for k, v in choices.items(): if v in inverse_choices: vollog.log( constants.LOGLEVEL_VVV, f"Enumeration value {v} duplicated as {k}. Keeping name {inverse_choices[v]}", ) continue inverse_choices[v] = k return inverse_choices
[docs] def lookup(self, value: Optional[int] = None) -> str: """Looks up an individual value and returns the associated name. If multiple identifiers map to the same value, the first matching identifier will be returned """ if value is None: return self.lookup(self) if value in self._inverse_choices: return self._inverse_choices[value] raise ValueError("The value of the enumeration is outside the possible choices")
@property def description(self) -> str: """Returns the chosen name for the value this object contains.""" return self.lookup(self) @property def choices(self) -> Dict[str, int]: return self._vol["choices"] @property def is_valid_choice(self) -> bool: """Returns whether the value for the object is a valid choice""" return self in self.choices.values() def __getattr__(self, attr: str) -> str: """Returns the value for a specific name.""" if attr in self._vol["choices"]: return self._vol["choices"][attr] raise AttributeError( f"Unknown attribute {attr} for Enumeration {self._vol['type_name']}" )
[docs] def write(self, value: bytes): raise NotImplementedError("Writing to Enumerations is not yet implemented")
[docs] class VolTemplateProxy(interfaces.objects.ObjectInterface.VolTemplateProxy): _methods = ["lookup"]
[docs] @classmethod def lookup(cls, template: interfaces.objects.Template, value: int) -> str: """Looks up an individual value and returns the associated name. If multiple identifiers map to the same value, the first matching identifier will be returned """ _inverse_choices = Enumeration._generate_inverse_choices( template.vol["choices"] ) if value in _inverse_choices: return _inverse_choices[value] raise ValueError( "The value of the enumeration is outside the possible choices" )
[docs] @classmethod def size(cls, template: interfaces.objects.Template) -> int: return template.vol["base_type"].size
[docs] @classmethod def children( cls, template: interfaces.objects.Template ) -> List[interfaces.objects.Template]: """Returns the children of the template.""" if "base_type" in template.vol: return [template.vol.base_type] return []
[docs] @classmethod def replace_child( cls, template: interfaces.objects.Template, old_child: interfaces.objects.Template, new_child: interfaces.objects.Template, ) -> None: """Substitutes the old_child for the new_child.""" if "base_type" in template.vol: if template.vol.base_type == old_child: template.update_vol(base_type=new_child)
[docs] class Array(interfaces.objects.ObjectInterface, collections.abc.Sequence): """Object which can contain a fixed number of an object type.""" def __init__( self, context: interfaces.context.ContextInterface, type_name: str, object_info: interfaces.objects.ObjectInformation, count: int = 0, subtype: Optional[templates.ObjectTemplate] = None, ) -> None: super().__init__(context=context, type_name=type_name, object_info=object_info) self._vol["count"] = count self._vol["subtype"] = subtype self._vol["size"] = 0 if subtype is not None: self._vol["size"] = count * subtype.size # This overrides the little known Sequence.count(val) that returns the number of items in the list that match val # Changing the name would be confusing (since we use count of an array everywhere else), so this is more important @property def count(self) -> int: """Returns the count dynamically.""" return self.vol.count @count.setter def count(self, value: int) -> None: """Sets the count to a specific value.""" self._vol["count"] = value self._vol["size"] = value * self._vol["subtype"].size def __repr__(self) -> str: """Describes the object appropriately""" return AggregateType.__repr__(self)
[docs] class VolTemplateProxy(interfaces.objects.ObjectInterface.VolTemplateProxy):
[docs] @classmethod def size(cls, template: interfaces.objects.Template) -> int: """Returns the size of the array, based on the count and the subtype.""" if "subtype" not in template.vol and "count" not in template.vol: raise ValueError( "Array ObjectTemplate must be provided a count and subtype" ) return template.vol.get("subtype", None).size * template.vol.get("count", 0)
[docs] @classmethod def children( cls, template: interfaces.objects.Template ) -> List[interfaces.objects.Template]: """Returns the children of the template.""" if "subtype" in template.vol: return [template.vol.subtype] return []
[docs] @classmethod def replace_child( cls, template: interfaces.objects.Template, old_child: interfaces.objects.Template, new_child: interfaces.objects.Template, ) -> None: """Substitutes the old_child for the new_child.""" if "subtype" in template.vol: if template.vol["subtype"] == old_child: template.update_vol(subtype=new_child)
[docs] @classmethod def relative_child_offset( cls, template: interfaces.objects.Template, child: str ) -> int: """Returns the relative offset from the head of the parent data to the child member.""" if "subtype" in template.vol and child == "subtype": return 0 raise IndexError(f"Member not present in array template: {child}")
[docs] @classmethod def child_template( cls, template: interfaces.objects.Template, child: str ) -> interfaces.objects.Template: """Returns the template of the child member.""" if "subtype" in template.vol and child == "subtype": return template.vol.subtype raise IndexError(f"Member not present in array template: {child}")
@overload def __getitem__(self, i: int) -> interfaces.objects.Template: ... @overload def __getitem__(self, s: slice) -> List[interfaces.objects.Template]: ... def __getitem__(self, i): """Returns the i-th item from the array.""" result: List[interfaces.objects.Template] = [] mask = self._context.layers[self.vol.layer_name].address_mask # We use the range function to deal with slices for us series = range(self.vol.count)[i] return_list = True if isinstance(series, int): return_list = False series = [series] for index in series: object_info = interfaces.objects.ObjectInformation( layer_name=self.vol.layer_name, offset=mask & (self.vol.offset + (self.vol.subtype.size * index)), parent=self, native_layer_name=self.vol.native_layer_name or self.vol.layer_name, size=self.vol.subtype.size, ) result += [self.vol.subtype(context=self._context, object_info=object_info)] if not return_list: return result[0] return result def __len__(self) -> int: """Returns the length of the array.""" return self.vol.count
[docs] def write(self, value) -> None: if not isinstance(value, collections.abc.Sequence): raise TypeError("Only Sequences can be written to arrays") self.count = len(value) for index in range(len(value)): self[index].write(value[index])
[docs] class AggregateType(interfaces.objects.ObjectInterface): """Object which can contain members that are other objects. Keep the number of methods in this class low or very specific, since each one could overload a valid member. """ def __init__( self, context: interfaces.context.ContextInterface, type_name: str, object_info: interfaces.objects.ObjectInformation, size: int, members: Dict[str, Tuple[int, interfaces.objects.Template]], ) -> None: super().__init__( context=context, type_name=type_name, object_info=object_info, size=size, members=members, ) # self._check_members(members) self._concrete_members: Dict[str, Dict] = {}
[docs] def has_member(self, member_name: str) -> bool: """Returns whether the object would contain a member called member_name.""" return member_name in self.vol.members
def __repr__(self) -> str: """Describes the object appropriately""" extras = member_name = "" if self.vol.native_layer_name != self.vol.layer_name: extras += f" (Native: {self.vol.native_layer_name})" if self.vol.member_name: member_name = f" (.{self.vol.member_name})" return f"<{self.__class__.__name__} {self.vol.type_name}{member_name}: {self.vol.layer_name} @ 0x{self.vol.offset:x} #{self.vol.size}{extras}>"
[docs] class VolTemplateProxy(interfaces.objects.ObjectInterface.VolTemplateProxy):
[docs] @classmethod def size(cls, template: interfaces.objects.Template) -> int: """Method to return the size of this type.""" if template.vol.get("size", None) is None: raise ValueError("ObjectTemplate not provided with a size") return template.vol.size
[docs] @classmethod def children( cls, template: interfaces.objects.Template ) -> List[interfaces.objects.Template]: """Method to list children of a template.""" return [member for _, member in template.vol.members.values()]
[docs] @classmethod def replace_child( cls, template: interfaces.objects.Template, old_child: interfaces.objects.Template, new_child: interfaces.objects.Template, ) -> None: """Replace a child elements within the arguments handed to the template.""" for member in template.vol.members.get("members", {}): relative_offset, member_template = template.vol.members[member] if member_template == old_child: # Members will give access to the mutable members list, # but in case that ever changes, do the update correctly tmp_list = template.vol.members tmp_list[member] = (relative_offset, new_child) # If there's trouble with mutability, consider making update_vol return a clone with the changes # (there will be a few other places that will be necessary) and/or making these part of the # permanent dictionaries rather than the non-cloneable ones template.update_vol(members=tmp_list)
[docs] @classmethod def relative_child_offset( cls, template: interfaces.objects.Template, child: str ) -> int: """Returns the relative offset of a child to its parent.""" retlist = template.vol.members.get(child, None) if retlist is None: raise IndexError(f"Member not present in template: {child}") return retlist[0]
[docs] @classmethod def child_template( cls, template: interfaces.objects.Template, child: str ) -> interfaces.objects.Template: """Returns the template of a child to its parent.""" retlist = template.vol.members.get(child, None) if retlist is None: raise IndexError(f"Member not present in template: {child}") return retlist[1]
[docs] @classmethod def has_member( cls, template: interfaces.objects.Template, member_name: str ) -> bool: """Returns whether the object would contain a member called member_name.""" return member_name in template.vol.members
@classmethod def _check_members( cls, members: Dict[str, Tuple[int, interfaces.objects.Template]] ) -> None: # Members should be an iterable mapping of symbol names to tuples of (relative_offset, ObjectTemplate) # An object template is a callable that when called with a context, offset, layer_name and type_name # We duplicate this code to avoid polluting the methodspace agg_name = "AggregateType" for agg_type in AggregateTypes: if isinstance(cls, agg_type): agg_name = agg_type.__name__ assert isinstance(members, collections.abc.Mapping), ( f"{agg_name} members parameter must be a mapping: {type(members)}" ) assert all( (isinstance(member, tuple) and len(member) == 2) for member in members.values() ), f"{agg_name} members must be a tuple of relative_offsets and templates"
[docs] def member(self, attr: str = "member") -> object: """Specifically named method for retrieving members.""" return self.__getattr__(attr)
def __getattr__(self, attr: str) -> Any: """Method for accessing members of the type.""" if attr in ["_concrete_members", "vol"]: raise AttributeError("Object has not been properly initialized") if attr in self._concrete_members: return self._concrete_members[attr] if attr.startswith("_") and not attr.startswith("__") and "__" in attr: attr = attr[attr.find("__", 1) :] # See issue #522 if attr in self.vol.members: mask = self._context.layers[self.vol.layer_name].address_mask relative_offset, template = self.vol.members[attr] if isinstance(template, templates.ReferenceTemplate): template = self._context.symbol_space.get_type(template.vol.type_name) object_info = interfaces.objects.ObjectInformation( layer_name=self.vol.layer_name, offset=mask & (self.vol.offset + relative_offset), member_name=attr, parent=self, native_layer_name=self.vol.native_layer_name or self.vol.layer_name, size=template.size, ) member = template(context=self._context, object_info=object_info) self._concrete_members[attr] = member return member # We duplicate this code to avoid polluting the methodspace agg_name = "AggregateType" for agg_type in AggregateTypes: if isinstance(self, agg_type): agg_name = agg_type.__name__ raise AttributeError( f"{agg_name} has no attribute: {self.vol.type_name}.{attr}" ) # Disable messing around with setattr until the consequences have been considered properly # For example pdbutil constructs objects and then sets values for them # Some don't always match the type (for example, the data read is encoded and interpreted) # # def __setattr__(self, name, value): # """Method for writing specific members of a structure""" # if name in ['_concrete_members', 'vol', '_vol'] or not self.has_member(name): # return super().__setattr__(name, value) # attr = self.__getattr__(name) # return attr.write(value) def __dir__(self) -> Iterable[str]: """Returns a complete list of members when dir is called.""" return list(super().__dir__()) + list(self.vol.members)
[docs] def write(self, value): # We duplicate this code to avoid polluting the methodspace agg_name = "AggregateType" for agg_type in AggregateTypes: if isinstance(self, agg_type): agg_name = agg_type.__name__ raise TypeError( f"{agg_name}s cannot be written to directly, individual members must be written instead" )
[docs] class StructType(AggregateType): pass
[docs] class UnionType(AggregateType): pass
[docs] class ClassType(AggregateType): pass
AggregateTypes = {StructType: "struct", UnionType: "union", ClassType: "class"}