Source code for memory.helpers

# ../memory/helpers.py

"""Provides helper classes/functions for memory functionality."""

# =============================================================================
# >> IMPORTS
# =============================================================================
# Python
#   Binascii
import binascii

# Source.Python
#   Core
from core import PLATFORM
#   Memory
from memory import Convention
from memory import DataType
from memory import Function
from memory import Pointer
from memory import TYPE_SIZES
from memory import make_object


# =============================================================================
# >> ALL DECLARATION
# =============================================================================
__all__ = ('Array',
           'BasePointer',
           'Key',
           'MemberFunction',
           'NO_DEFAULT',
           'Type',
           'parse_data',
           )


# =============================================================================
# >> Type
# =============================================================================
[docs]class Type(object): """Stores attribute/array types.""" BOOL = 'bool' CHAR = 'char' UCHAR = 'uchar' SHORT = 'short' USHORT = 'ushort' INT = 'int' UINT = 'uint' LONG = 'long' ULONG = 'ulong' LONG_LONG = 'long_long' ULONG_LONG = 'ulong_long' FLOAT = 'float' DOUBLE = 'double' POINTER = 'pointer' STRING_POINTER = 'string_pointer' STRING_ARRAY = 'string_array' @staticmethod
[docs] def is_native(type_name): """Return True if the given type name is a native type.""" return hasattr(Type, type_name.upper())
# ============================================================================= # >> Key # =============================================================================
[docs]class Key(object): """Holds some constants and provides converters for parse_data().""" # General type information keys BINARY = 'binary' SRV_CHECK = 'srv_check' SIZE = 'size' OFFSET = 'offset' # Attribute/array keys TYPE_NAME = 'type' # Array keys LENGTH = 'length' # Pointer keys LEVEL = 'level' ACCESSOR = 'accessor' ACCESSOR_OFFSET = 'accessor_offset' # (Virtual) function keys ARGS = 'arguments' RETURN_TYPE = 'return_type' CONVENTION = 'convention' IDENTIFIER = 'identifier' # Shared keys DOC = 'doc' @staticmethod
[docs] def as_bool(manager, value): """Convert a string to a boolean. Raises a ValueError if the string doesn't represent such a value. """ value = value.lower() if value == 'true': return True if value == 'false': return False raise ValueError( 'Cannot convert "{0}" to a boolean value.'.format(value))
@staticmethod
[docs] def as_args_tuple(manager, value): """Convert a string into a tuple containing <DataType> elements.""" if isinstance(value, str): return (DataType.names[value], ) return tuple(DataType.names[item] for item in value)
@staticmethod
[docs] def as_return_type(manager, value): """Convert a string into a <Return> object. If the conversion fails, the string itself will be returned. """ return DataType.names.get(value, value)
@staticmethod
[docs] def as_identifier(manager, value): """Convert a string into a byte string. If no spaces in the string, the string itself will be returned. """ if ' ' in value: return binascii.unhexlify(value.replace(' ', '')) return value
@staticmethod
[docs] def as_convention(manager, value): """Convert a string into a <Convention> object.""" try: return Convention.names[value] except KeyError: return manager.custom_conventions[value]
@staticmethod
[docs] def as_attribute_type(manager, value): """Convert a string into a <Type> value.""" if Type.is_native(value): return getattr(Type, value) return value
@staticmethod
[docs] def as_str(manager, value): """Convert the value to a string.""" return str(value)
@staticmethod
[docs] def as_int(manager, value): """Convert the value to an integer.""" try: return int(value) except ValueError: return int(value, 16)
# ============================================================================= # >> BasePointer # =============================================================================
[docs]class BasePointer(Pointer): """Pointer extension class.""" # These four operator functions are required. Otherwise we would downcast # the instance to the Pointer class if we add or subtract bytes. # TODO: Can we do that on the C++ side? # If yes, this class would be redundant. def __add__(self, other): """Return self+value.""" return make_object(self.__class__, Pointer(int(self) + int(other))) def __radd__(self, other): """Return value+self.""" return self + other def __sub__(self, other): """Return self-value.""" return make_object(self.__class__, Pointer(int(self) - int(other))) def __rsub__(self, other): """Return value-self.""" return self - other
# ============================================================================= # >> Array # =============================================================================
[docs]class Array(BasePointer): """Wrap an array."""
[docs] def __init__(self, manager, is_ptr, type_name, ptr, length=None): """Initialize the array wrapper. :param TypeManager manager: The manager that should be used to retrieve classes. :param bool is_ptr: Set to True if the array contains pointers. :param str type_name: The name of the array type. E.g. 'Vector' or 'bool'. :param Pointer ptr: The base address of the array (the very first array entry). :param int|None length: Length of the array. Setting this value allows you to iterate over the array. """ self._manager = manager # Set to True if the array contains pointers, else False self._is_ptr = is_ptr # Contains the type name of the array self._type_name = type_name # Optional -- specifies the length of the array self._length = length super().__init__(ptr)
def __getitem__(self, index): """Return the value at the given index.""" return self._make_attribute(index).__get__(self) def __setitem__(self, index, value): """Set the value at the given index.""" self._make_attribute(index).__set__(self, value) def __iter__(self): """Return a generator that can iterate over the array.""" # This prevents users from iterating over the array without having # _length specified. Otherwise the server would hang or crash. if self._length is None: raise ValueError( 'Cannot iterate over the array without _length being specif' + 'ied.') for index in range(self._length): yield self[index] def _make_attribute(self, index): """Validate the index and returns a new property object.""" # Validate the index, so we don't access invalid memory addresses if self._length is not None and index >= self._length: raise IndexError('Index out of range') # Construct the proper function name name = ('pointer' if self._is_ptr else 'instance') + '_attribute' # Get the function and call it return getattr(self._manager, name)( self._type_name, self.get_offset(index) )
[docs] def get_offset(self, index): """Return the offset of the given index.""" # Pointer arrays always have every 4 bytes a new pointer if self._is_ptr: return index * TYPE_SIZES['POINTER'] # Every 1, 2, 4 or 8 bytes is a new value if Type.is_native(self._type_name): return index * TYPE_SIZES[self._type_name.upper()] # Get the class of the custom type cls = self._manager.get_class(self._type_name) if cls is None: raise NameError('Unknown class "{0}".'.format(self._type_name)) # To access a value, we require the proper size of a custom type if cls._size is None: raise ValueError('Array requires a size to access its values.') # Every x bytes is a new instance return index * cls._size
# Arrays have another constructor and we don't want to downcast. So, we # have to implement these operators here again. def __add__(self, other): """Add bytes or another pointer to the base address.""" return self.__class__( self._manager, self._is_ptr, self._type_name, int(self) + int(other), self._length ) def __sub__(self, other): """Subtract bytes or another pointer from the base address.""" return self.__class__( self._manager, self._is_ptr, self._type_name, int(self) - int(other), self._length )
# ============================================================================= # >> MemberFunction # =============================================================================
[docs]class MemberFunction(Function): """Use this class to create a wrapper for member functions. It passes the this pointer automatically to the wrapped function. """
[docs] def __init__(self, manager, return_type, func, this): """Initialize the instance.""" self._function = func super().__init__(func) # This should always hold a TypeManager instance self._manager = manager # Holds the this pointer self._this = this # Holds the return type name self._type_name = return_type
def __call__(self, *args): """Call the function dynamically.""" return super().__call__(self._this, *args)
[docs] def call_trampoline(self, *args): """Call the trampoline dynamically.""" return super().call_trampoline(self._this, *args)
[docs] def skip_hooks(self, *args): """Call the function, but skip hooks if there are any.""" return super().skip_hooks(self._this, *args)
# ============================================================================= # >> FUNCTIONS # =============================================================================
[docs]def parse_data(manager, raw_data, keys): """Parse the data dictionary. Parses by converting the values of the given keys into the proper type or assigning them default values. Raises a KeyError if a key does not exist and if no default value is available. Returns a generator: (<name>, [<value of key0>, <value of key1>, ...]) <keys> must have the following structure: ((<key name>, <converter>, <default value>), ...) The convert function must accept 2 arguments: 1. An instance of the TypeManager class 2. The value to convert Information about data that comes from a file: You can specialize every key by adding ''_windows'' (for Windows) or ''_linux'' (for Linux) to the end a key. For example: If you are using a signature on Windows, but a symbol on Linux, you have three possibilities to do that: 1. identifier_windows = <signature for Windows> identifier = <symbol for Linux> 2. identifier = <signature for Windows> identifier_linux = <symbol for Linux> 3. identifier_windows = <signature for Windows> identifier_linux = <symbol for Linux> """ for name, data in raw_data.items(): temp_data = [] for key, converter, default in keys: # Get the OS specific key. If that fails, fall back to the shared # key. If that fails too, use the default value value = data.get(key + '_' + PLATFORM, data.get(key, default)) # If the value is NO_DEFAULT, the key is obviously missing if value is NO_DEFAULT: raise KeyError( 'Missing information for key "{0}".'.format(key)) temp_data.append( value if value is default else converter(manager, value)) yield (name, temp_data)
# Use this as a default value if the key is not allowed to have a default # value NO_DEFAULT = object()