Skip to content

Rework protocols and buildprotocols.py tool. Now a protocol can be loaded during runtime, either in batch from a file, or individually. #105

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,213 changes: 2,213 additions & 0 deletions pyobjus/_default_protocols.py

Large diffs are not rendered by default.

2,305 changes: 93 additions & 2,212 deletions pyobjus/protocols.py

Large diffs are not rendered by default.

550 changes: 324 additions & 226 deletions tools/buildprotocols.py
Original file line number Diff line number Diff line change
@@ -1,231 +1,329 @@
'''
"""
Build an informationnal protocol list, based on the objective-c headers.
"""

import argparse
import json
import os


class FrameworkDiscover:
"""
Looks for frameworks (framework, xcframework) in a directory.
"""

def __init__(self):
self._frameworks = {}

def _search_frameworks(self, directory):
if not os.path.exists(directory):
return

for fn in os.listdir(directory):
if fn.endswith(".framework") or fn.endswith(".xcframework"):
self._frameworks[fn] = os.path.join(directory, fn)

def search(self, directory):
"""
Search for frameworks in a directory.
:param directory: The directory to search in.
:return: A dictionary with the frameworks found.
"""
self._frameworks = {}
self._search_frameworks(directory)
return self._frameworks


class FrameworkProtocolsScanner:
"""
This class is used to scan a framework for protocols.
"""

def __init__(self, framework_path: str):
"""
:param framework_path: The framework path
(e.g. /System/Library/Frameworks/AddressBook.framework)
"""
self._framework_path = framework_path
self._results = {}

def _search_frameworks(self, directory):
for _framework_path in FrameworkDiscover().search(directory).values():
self._scan_framework(_framework_path)

def _scan_framework(self, framework_path):
print("Scanning framework", framework_path)

if framework_path.endswith(".framework"):
self._scan_headers(framework_path, framework_path)
_subframeworks_path = os.path.join(framework_path, "Frameworks")
self._search_frameworks(_subframeworks_path)
elif framework_path.endswith(".xcframework"):
for _arch_dir in os.listdir(framework_path):
_arch_path = os.path.join(framework_path, _arch_dir)
if os.path.isdir(_arch_path):
self._search_frameworks(_arch_path)

def _scan_headers(self, framework, pathfn):
headers = os.path.join(pathfn, "Headers")
if not os.path.exists(headers):
return
for fn in os.listdir(headers):
fullfn = os.path.join(headers, fn)
if os.path.isdir(fn):
self._scan_headers(framework, fullfn)
elif fn.endswith(".h"):
self._scan_header(framework, fullfn)

def _scan_header(self, framework, header_fn):
framework_name = os.path.basename(framework).rsplit(".", 1)[0]
with open(header_fn) as fd:
lines = fd.readlines()

protocol = None
for line in lines:
line = line.strip()

if protocol is None:
if line.startswith("@protocol") and not line.endswith(";"):
# extract protocol name
print(framework_name, line)
try:
protocol_name = line.split()[1]
except IndexError:
# not the droids we're looking for
pass
else:
protocol = (protocol_name, [])
else:
if line.startswith("@end"):
# done, insert the protocol!
self._insert_protocol(framework_name, protocol)
protocol = None
elif line.startswith("-") and ":" in line:
try:
delegate = self._parse_delegate(line)
except Exception:
pass
else:
protocol[1].append(delegate)

def _insert_protocol(self, framework_name, protocol):
if framework_name not in self._results:
self._results[framework_name] = {}
protocol_name, delegates = protocol
self._results[framework_name][protocol_name] = delegates

@staticmethod
def _tokenize_delegate(line):
token = ""
while line:
# print 'tokenize', line
if line[0] == "(":
if token:
yield token
token = ""
end = line.index(")")
yield line[: end + 1]
line = line[end + 1 :]
continue

if line[0] == " " or line[0] == ";":
if token:
yield token
token = ""
line = line[1:]
continue

token = token + line[0]
line = line[1:]

FIXME: early version, do not use unless you known what you're doing.
'''

from os import listdir, environ
from os.path import join, exists, isdir, basename

protocols = {}
DEBUG = environ.get('PYOBJUS_DEBUG', '0') == '1'

def dprint(*args):
if not DEBUG:
return
print args


def search_frameworks(directory):
for fn in listdir(directory):
if not fn.endswith('.framework'):
continue
scan_framework(join(directory, fn))


def scan_framework(framework):
scan_headers(framework, framework)
frameworks = join(framework, 'Frameworks')
if exists(frameworks):
search_frameworks(frameworks)


def scan_headers(framework, pathfn):
headers = join(pathfn, 'Headers')
if not exists(headers):
return
for fn in listdir(headers):
fullfn = join(headers, fn)
if isdir(fn):
scan_headers(framework, fullfn)
elif fn.endswith('.h'):
scan_header(framework, fullfn)


def scan_header(framework, header_fn):
framework_name = basename(framework).rsplit('.', 1)[0]
with open(header_fn) as fd:
lines = fd.readlines()

protocol = None
for line in lines:
line = line.strip()

if protocol is None:
if line.startswith('@protocol') and not line.endswith(';'):
# extract protocol name
dprint(framework_name, line)
try:
protocol_name = line.split()[1]
except IndexError:
# not the droids we're looking for
pass
else:
protocol = (protocol_name, [])
if token:
yield token

@staticmethod
def _convert_type_to_signature(token):
METHOD_ENCODINGS = dict(
[
("const", "r"),
("in", "n"),
("inout", "N"),
("out", "o"),
("bycopy", "O"),
("byref", "R"),
("oneway", "V"),
]
)

sig = ""
size = (4, 8)
tokens = token.split(" ")
# print 'convert_type_to_signature()', tokens
while True:
t = tokens[0]
if t not in METHOD_ENCODINGS:
break
sig += METHOD_ENCODINGS[t]
tokens = tokens[1:]

token = " ".join(tokens)

if token in ("BOOL",):
sig += "B"
elif token in ("char",):
sig += "c"
elif token in ("int", "NSInteger"):
sig += "i"
elif token in ("long",):
sig += "l"
elif token in ("long long",):
sig += "q"
elif token in ("unsigned char",):
sig += "C"
elif token in ("unsigned int", "NSUInteger"):
sig += "I"
elif token in ("unsigned short",):
sig += "S"
elif token in ("unsigned long",):
sig += "L"
elif token in ("unsigned long long",):
sig += "Q"
elif token in ("float", "CGFloat"):
sig += "f"
elif token in ("double", "CGDouble"):
sig += "d"
elif token in ("char *",):
sig += "*"
elif token == "void":
sig += "v"
elif token == "id":
sig += "@"
else:
if line.startswith('@end'):
# done, insert the protocol!
insert_protocol(framework_name, protocol)
protocol = None
elif line.startswith('-') and ':' in line:
try:
delegate = parse_delegate(line)
except Exception:
pass
else:
protocol[1].append(delegate)


def insert_protocol(framework_name, protocol):
if framework_name not in protocols:
protocols[framework_name] = {}
protocol_name, delegates = protocol
protocols[framework_name][protocol_name] = delegates
#print 'find', protocol_name, delegates


def parse_delegate(line):

if line.startswith('- '):
line = line[2:]
elif line.startswith('-'):
line = line[1:]

dprint('----', line)
fn = ''
sig = []
for index, token in enumerate(tokenize_delegate(line)):
dprint('--->', token)
if ':' in token:
if index == 1:
sig.extend([
('@', (4, 8)),
(':', (4, 8))])
if token != ':':
fn += token
elif token[0] == '(':
sig.append(convert_type_to_signature(token[1:-1]))
elif token.upper() == token:
# end?
break
sig32 = build_signature(sig, 0)
sig64 = build_signature(sig, 1)
dprint('---- selector', fn, sig32, sig64)

#if 'plugInDidAcceptOutgoingFileTransferSession' in fn:
# import sys; sys.exit(0)

return (fn, sig32, sig64)


def build_signature(items, index):
sig = ''
offset = 0
for tp, sizes in items[1:]:
sig += '{}{}'.format(tp, offset)
offset += sizes[index]

sig = '{}{}'.format(items[0][0], offset) + sig
return sig



def tokenize_delegate(line):
token = ''
while line:
#print 'tokenize', line
if line[0] == '(':
if token:
yield token
token = ''
end = line.index(')')
yield line[:end + 1]
line = line[end + 1:]
continue

if line[0] == ' ' or line[0] == ';':
if token:
yield token
token = ''
print("Unknown type: {!r}".format(token))
# assert(0)
sig += "@"

return (sig, size)

@staticmethod
def _build_signature(items, index):
sig = ""
offset = 0
for tp, sizes in items[1:]:
sig += "{}{}".format(tp, offset)
offset += sizes[index]

sig = "{}{}".format(items[0][0], offset) + sig
return sig

def _parse_delegate(self, line):
if line.startswith("- "):
line = line[2:]
elif line.startswith("-"):
line = line[1:]
continue

token = token + line[0]
line = line[1:]

if token:
yield token


method_encodings = dict([
('const', 'r'), ('in', 'n'), ('inout', 'N'), ('out', 'o'), ('bycopy', 'O'),
('byref', 'R'), ('oneway', 'V')])

def convert_type_to_signature(token):
sig = ''
size = (4, 8)
tokens = token.split(' ')
#print 'convert_type_to_signature()', tokens
while True:
t = tokens[0]
if t not in method_encodings:
break
sig += method_encodings[t]
tokens = tokens[1:]

token = ' '.join(tokens)

if token in ('BOOL', ):
sig += 'B'
elif token in ('char', ):
sig += 'c'
elif token in ('int', 'NSInteger'):
sig += 'i'
elif token in ('long', ):
sig += 'l'
elif token in ('long long', ):
sig += 'q'
elif token in ('unsigned char', ):
sig += 'C'
elif token in ('unsigned int', 'NSUInteger'):
sig += 'I'
elif token in ('unsigned short', ):
sig += 'S'
elif token in ('unsigned long', ):
sig += 'L'
elif token in ('unsigned long long', ):
sig += 'Q'
elif token in ('float', 'CGFloat'):
sig += 'f'
elif token in ('double', 'CGDouble'):
sig += 'd'
elif token in ('char *', ):
sig += '*'
elif token == 'void':
sig += 'v'
elif token == 'id':
sig += '@'
else:
dprint('Unknown type: {!r}'.format(token))
#assert(0)
sig += '@'

return (sig, size)

if __name__ == '__main__':
from os.path import dirname
search_frameworks('/System/Library/Frameworks')
fn = join(dirname(__file__), '..', 'pyobjus', 'protocols.py')
with open(fn, 'w') as fd:
fd.write('# autogenerated by buildprotocols.py\n')
fd.write('protocols = {\n')
for items in protocols.values():
for protocol, delegates in items.items():
fd.write(' "{}": {{\n'.format(protocol))
for delegate, sig32, sig64 in delegates:
fd.write(' "{}": ("{}", "{}"),\n'.format(
delegate, sig32, sig64))
fd.write(' },\n')
fd.write('}')


print("----", line)
fn = ""
sig = []
for index, token in enumerate(self._tokenize_delegate(line)):
print("--->", token)
if ":" in token:
if index == 1:
sig.extend([("@", (4, 8)), (":", (4, 8))])
if token != ":":
fn += token
elif token[0] == "(":
sig.append(self._convert_type_to_signature(token[1:-1]))
elif token.upper() == token:
# end?
break
sig32 = self._build_signature(sig, 0)
sig64 = self._build_signature(sig, 1)
print("---- selector", fn, sig32, sig64)

# if 'plugInDidAcceptOutgoingFileTransferSession' in fn:
# import sys; sys.exit(0)

return (fn, sig32, sig64)

def scan(self) -> dict:
"""
Scan the framework for protocols.
:return: A dictionary with the protocols found.
"""

self._results = {}

self._scan_framework(self._framework_path)

return self._results


def main():
parser = argparse.ArgumentParser(
description="Scan a framework for protocols."
)
parser.add_argument(
"-f",
"--framework",
metavar="FRAMEWORK",
type=str,
nargs="+",
default=[],
help="framework(s) to analyze",
)
parser.add_argument(
"-d",
"--directory",
metavar="DIRECTORY",
type=str,
nargs="+",
default=[],
help="directory(ies) to analyze for frameworks",
)
parser.add_argument(
"-o",
"--output",
metavar="OUTPUT",
type=str,
default="pyobjus_extra_protocols.json",
help="output file name (default: pyobjus_extra_protocols.json)",
)
args = parser.parse_args()

results = {}

_frameworks = []
_frameworks.extend(args.framework)

for _directory in args.directory:
_frameworks.extend(FrameworkDiscover().search(_directory))

for _framework in _frameworks:
scanner = FrameworkProtocolsScanner(_framework)
_framework_results = scanner.scan()

for framework_protocols in _framework_results.values():
for protocol, methods in framework_protocols.items():
if protocol not in results:
results[protocol] = {}
for (
method_name,
method_sig_32,
method_sig_64,
) in methods:
results[protocol][method_name] = {
"signatures": {
"32": method_sig_32,
"64": method_sig_64,
}
}

json.dump(results, open(args.output, "w"))


if __name__ == "__main__":
main()