from contextlib import suppress
from dataclasses import dataclass
from functools import partial
from pathlib import Path
from typing import Optional
from pygments import highlight
from pygments.formatters import TerminalTrueColorFormatter
from pygments.lexers import ObjectiveCLexer
from rpcclient.darwin import objc
from rpcclient.darwin.objective_c_class import Class
from rpcclient.darwin.symbol import DarwinSymbol
from rpcclient.exceptions import RpcClientException
from rpcclient.symbol import Symbol
from rpcclient.symbols_jar import SymbolsJar
[docs]
class SettingIvarError(RpcClientException):
""" Raise when trying to set an Ivar too early or when the Ivar doesn't exist. """
pass
[docs]
@dataclass
class Ivar:
name: str
value: DarwinSymbol
type_: str
offset: int
[docs]
class ObjectiveCSymbol(DarwinSymbol):
"""
Wrapper object for an objective-c symbol.
Allowing easier access to its properties, methods and ivars.
"""
[docs]
@classmethod
def create(cls, value: int, client):
"""
Create an ObjectiveCSymbol object.
:param value: Symbol address.
:param rpcclient.darwin.client.Client client: client.
:return: ObjectiveCSymbol object.
:rtype: ObjectiveCSymbol
"""
symbol = super().create(value, client)
symbol.ivars = []
symbol.properties = []
symbol.methods = []
symbol.class_ = None # type: Class
symbol.reload()
return symbol
[docs]
def reload(self):
"""
Reload object's in-memory layout.
"""
object_data = self._client.showobject(self)
ivars_list = [
Ivar(name=ivar['name'], type_=ivar['type'], offset=ivar['offset'],
value=ivar['value']) for ivar in object_data['ivars']
]
methods_list = [
objc.Method.from_data(method, self._client) for method in object_data['methods']
]
properties_list = [
objc.Property(name=prop['name'], attributes=objc.convert_encoded_property_attributes(prop['attributes']))
for prop in object_data['properties']
]
class_object = Symbol.create(object_data['class_address'], self._client)
class_wrapper = Class(self._client, class_object)
self.ivars = ivars_list
self.methods = methods_list
self.properties = properties_list
self.class_ = class_wrapper
[docs]
def show(self, dump_to: Optional[str] = None, recursive: bool = False):
"""
Print to terminal the highlighted class description.
:param dump_to: directory to dump.
:param recursive: Show methods of super classes.
"""
formatted = self._to_str(recursive)
print(highlight(formatted, ObjectiveCLexer(), TerminalTrueColorFormatter(style='native')))
if dump_to is None:
return
(Path(dump_to) / f'{self.class_.name}.m').expanduser().write_text(formatted)
[docs]
def objc_call(self, selector: str, *params, **kwargs):
"""
Make objc_call() from self return ObjectiveCSymbol when it's an objc symbol.
:param selector: Selector to execute.
:param params: Additional parameters.
:return: ObjectiveCSymbol when return type is an objc symbol.
"""
symbol = super(ObjectiveCSymbol, self).objc_call(selector, *params, **kwargs)
return symbol.objc_symbol if self._client.is_objc_type(symbol) else symbol
def _set_ivar(self, name, value):
try:
ivars = self.__getattribute__('ivars')
class_name = self.__getattribute__('class_').name
except AttributeError as e:
raise SettingIvarError from e
for i, ivar in enumerate(ivars):
if ivar.name == name:
size = self.item_size
if i < len(self.ivars) - 1:
size = ivars[i + 1].offset - ivar.offset
with self.change_item_size(size):
self[ivar.offset // size] = value
ivar.value = value
return
raise SettingIvarError(f'Ivar "{name}" does not exist in "{class_name}"')
def _to_str(self, recursive=False):
protocols_buf = f'<{",".join(self.class_.protocols)}>' if self.class_.protocols else ''
if self.class_.super is not None:
buf = f'@interface {self.class_.name}: {self.class_.super.name} {protocols_buf}\n'
else:
buf = f'@interface {self.class_.name} {protocols_buf}\n'
# Add ivars
buf += '{\n'
for ivar in self.ivars:
buf += f'\t{ivar.type_} {ivar.name} = 0x{int(ivar.value):x}; // 0x{ivar.offset:x}\n'
buf += '}\n'
# Add properties
for prop in self.properties:
attrs = prop.attributes
buf += f'@property ({",".join(attrs.list)}) {prop.attributes.type_} {prop.name};\n'
if attrs.synthesize is not None:
buf += f'@synthesize {prop.name} = {attrs.synthesize};\n'
# Add methods
methods = self.methods.copy()
# Add super methods.
if recursive:
for sup in self.class_.iter_supers():
for method in filter(lambda m: m not in methods, sup.methods):
methods.append(method)
# Print class methods first.
methods.sort(key=lambda m: not m.is_class)
for method in methods:
buf += str(method)
buf += '@end'
return buf
@property
def symbols_jar(self) -> SymbolsJar:
""" Get a SymbolsJar object for quick operations on all methods """
jar = SymbolsJar.create(self._client)
for m in self.methods:
jar[m.name] = m.address
return jar
def __dir__(self):
result = set()
for ivar in self.ivars:
result.add(ivar.name)
for method in self.methods:
result.add(method.name.replace(':', '_'))
for sup in self.class_.iter_supers():
for method in sup.methods:
result.add(method.name.replace(':', '_'))
result.update(list(super(ObjectiveCSymbol, self).__dir__()))
return list(result)
def __getitem__(self, item):
if isinstance(item, int):
return super(ObjectiveCSymbol, self).__getitem__(item)
# Ivars
for ivar in self.ivars:
if ivar.name == item:
if self._client.is_objc_type(ivar.value):
return ivar.value.objc_symbol
return ivar.value
# Properties
for prop in self.properties:
if prop.name == item:
return self.objc_call(item)
# Methods
for method in self.methods:
if method.name == item:
return partial(self.class_.objc_call, item) if method.is_class else partial(self.objc_call, item)
for sup in self.class_.iter_supers():
for method in sup.methods:
if method.name == item:
return partial(self.class_.objc_call, item) if method.is_class else partial(self.objc_call, item)
raise AttributeError(f''''{self.class_.name}' has no attribute {item}''')
def __getattr__(self, item: str):
return self[self.class_.sanitize_name(item)]
def __setitem__(self, key, value):
if isinstance(key, int):
super(ObjectiveCSymbol, self).__setitem__(key, value)
return
with suppress(SettingIvarError):
self._set_ivar(key, value)
return
def __setattr__(self, key, value):
try:
key = self.__getattribute__('class_').sanitize_name(key)
except AttributeError:
pass
try:
self._set_ivar(key, value)
except SettingIvarError:
super(ObjectiveCSymbol, self).__setattr__(key, value)
def __str__(self):
return self._to_str(False)