Source code for rpcclient.darwin.objective_c_class

from collections import namedtuple
from functools import partial
from pathlib import Path
from typing import Mapping, Optional

from pygments import highlight
from pygments.formatters import TerminalTrueColorFormatter
from pygments.lexers import ObjectiveCLexer

from rpcclient.darwin import objc
from rpcclient.exceptions import GettingObjectiveCClassError
from rpcclient.symbols_jar import SymbolsJar

Ivar = namedtuple('Ivar', 'name type_ offset')


[docs] class Class: """ Wrapper for ObjectiveC Class object. """ def __init__(self, client, class_object=0, class_data: Mapping = None, lazy=False): """ :param rpcclient.darwin.client.DarwinClient client: Darwin client. :param rpcclient.darwin.objective_c_symbol.Symbol class_object: """ self._client = client self._class_object = class_object self.protocols = [] self.ivars = [] self.properties = [] self.methods = [] self.name = '' self.super = None if not lazy: if class_data is None: self.reload() else: self._load_class_data(class_data)
[docs] @staticmethod def from_class_name(client, class_name: str): """ Create ObjectiveC Class from given class name. :param rpcclient.darwin.client.DarwinClient client: Darwin client. :param class_name: Class name. """ class_object = client.symbols.objc_getClass(class_name) class_symbol = Class(client, class_object) if class_symbol.name != class_name: raise GettingObjectiveCClassError() return class_symbol
[docs] @staticmethod def sanitize_name(name: str): """ Sanitize python name to ObjectiveC name. """ if name.startswith('_'): name = '_' + name[1:].replace('_', ':') else: name = name.replace('_', ':') return name
[docs] def reload(self): """ Reload class object data. Should be used whenever the class layout changes (for example, during method swizzling) """ objc_class = self._class_object if self._class_object else self._client.symbols.objc_getClass(self.name) class_description = self._client.showclass(objc_class) self.super = Class(self._client, class_description['super']) if class_description['super'] else None self.name = class_description['name'] self.protocols = class_description['protocols'] self.ivars = [ Ivar(name=ivar['name'], type_=ivar['type'], offset=ivar['offset']) for ivar in class_description['ivars'] ] self.properties = [ objc.Property(name=prop['name'], attributes=objc.convert_encoded_property_attributes(prop['attributes'])) for prop in class_description['properties'] ] self.methods = [ objc.Method.from_data(method, self._client) for method in class_description['methods'] ]
[docs] def show(self, dump_to: Optional[str] = None): """ Print to terminal the highlighted class description. :param dump_to: directory to dump. """ formatted = str(self) print(highlight(formatted, ObjectiveCLexer(), TerminalTrueColorFormatter(style='native'))) if dump_to is None: return (Path(dump_to) / f'{self.name}.m').expanduser().write_text(formatted)
[docs] def objc_call(self, sel: str, *args, **kwargs): """ Invoke a selector on the given class object. :param sel: Selector name. :return: whatever the selector returned as a symbol. """ return self._class_object.objc_call(sel, *args, **kwargs)
[docs] def get_method(self, name: str): """ Get a specific method implementation. :param name: Method name. :return: Method. """ for method in self.methods: if method.name == name: return method
[docs] def iter_supers(self): """ Iterate over the super classes of the class. """ sup = self.super while sup is not None: yield sup sup = sup.super
def _load_class_data(self, data: Mapping): self._class_object = self._client.symbol(data['address']) self.super = Class(self._client, data['super']) if data['super'] else None self.name = data['name'] self.protocols = data['protocols'] self.ivars = [Ivar(name=ivar['name'], type_=ivar['type'], offset=ivar['offset']) for ivar in data['ivars']] self.properties = data['properties'] self.methods = data['methods'] @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[f'[{self.name} {m.name}]'] = m.address return jar @property def bundle_path(self) -> Path: return Path(self._client.symbols.objc_getClass('NSBundle') .objc_call('bundleForClass:', self._class_object).objc_call('bundlePath').py()) def __dir__(self): result = set() for method in self.methods: if method.is_class: result.add(method.name.replace(':', '_')) for sup in self.iter_supers(): for method in sup.methods: if method.is_class: result.add(method.name.replace(':', '_')) result.update(list(super(Class, self).__dir__())) return list(result) def __str__(self): protocol_buf = f'<{",".join(self.protocols)}>' if self.protocols else '' if self.super is not None: buf = f'@interface {self.name}: {self.super.name} {protocol_buf}\n' else: buf = f'@interface {self.name} {protocol_buf}\n' # Add ivars buf += '{\n' for ivar in self.ivars: buf += f'\t{ivar.type_} {ivar.name}; // 0x{ivar.offset:x}\n' buf += '}\n' # Add properties for prop in self.properties: buf += f'@property ({",".join(prop.attributes.list)}) {prop.attributes.type_} {prop.name};\n' if prop.attributes.synthesize is not None: buf += f'@synthesize {prop.name} = {prop.attributes.synthesize};\n' # Add methods for method in self.methods: buf += str(method) buf += '@end' return buf def __repr__(self): return f'<objC Class "{self.name}">' def __getitem__(self, item): for method in self.methods: if method.name == item: if method.is_class: return partial(self.objc_call, item) else: raise AttributeError(f'{self.name} class has an instance method named {item}, ' f'not a class method') for sup in self.iter_supers(): for method in sup.methods: if method.name == item: if method.is_class: return partial(self.objc_call, item) else: raise AttributeError(f'{self.name} class has an instance method named {item}, ' f'not a class method') raise AttributeError(f''''{self.name}' class has no attribute {item}''') def __getattr__(self, item: str): return self[self.sanitize_name(item)]