-
Notifications
You must be signed in to change notification settings - Fork 37
Expand file tree
/
Copy pathhelpers.py
More file actions
executable file
·409 lines (318 loc) · 12.4 KB
/
helpers.py
File metadata and controls
executable file
·409 lines (318 loc) · 12.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
# ../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
# =============================================================================
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
def is_native(type_name):
"""Return True if the given type name is a native type."""
return hasattr(Type, type_name.upper())
# =============================================================================
# >> Key
# =============================================================================
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
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
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
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
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
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
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
def as_str(manager, value):
"""Convert the value to a string."""
return str(value)
@staticmethod
def as_int(manager, value):
"""Convert the value to an integer."""
try:
return int(value)
except ValueError:
return int(value, 16)
# =============================================================================
# >> BasePointer
# =============================================================================
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
# =============================================================================
class Array(BasePointer):
"""Wrap an array."""
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)
)
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
# =============================================================================
class MemberFunction(Function):
"""Use this class to create a wrapper for member functions.
It passes the this pointer automatically to the wrapped function.
"""
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)
def call_trampoline(self, *args):
"""Call the trampoline dynamically."""
return super().call_trampoline(self._this, *args)
def skip_hooks(self, *args):
"""Call the function, but skip hooks if there are any."""
return super().skip_hooks(self._this, *args)
# =============================================================================
# >> FUNCTIONS
# =============================================================================
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()