├── .gitignore ├── README.md ├── To do.txt ├── cylogos.py ├── examples ├── DoubleHomeAlert.cy └── PowerDownLogger.cy ├── loader ├── CyLogos.plist ├── Makefile ├── Tweak.xm └── layout │ ├── DEBIAN │ └── control │ └── usr │ └── bin │ └── cylogos ├── oldVersion.py └── testCases ├── invalidTests ├── test.cy ├── test2.cy ├── test3.cy └── test4.cy ├── runTests.sh └── validTests ├── test.cy ├── test2.cy ├── test3.cy ├── test4.cy └── test5.cy /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | loader/debs 3 | loader/obj 4 | loader/.theos 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CyLogos 2 | 3 | A preprocessor + loader that allows use of Logos syntax with Cycript. Simply drop .cy files in `/Library/CyLogos/Tweaks` to have them automatically preprocessed and loaded into SpringBoard. Only %hook, %end, and %orig are supported currently. 4 | 5 | Check out [Cycript.org](http://www.cycript.org) and [the iPhone Dev Wiki Logos page](http://iphonedevwiki.net/index.php/Logos) for more information on Cycript and Logos. 6 | 7 | ### Example 8 | Example file before preprocessing (based on [this cycript demo](http://www.cycript.org/manual/#6635fc86-3c73-4176-b0b0-75dd3aa99ce3)): 9 | ```javascript 10 | %hook NSObject 11 | 12 | function description() { 13 | return %orig + " (of doom)"; 14 | } 15 | 16 | %end 17 | ``` 18 | 19 | Preprocessed: 20 | ```javascript 21 | var oldm = {}; 22 | MS.hookMessage(NSObject, @selector(description), function() { 23 | return oldm->call(this) + " (of doom)"; 24 | }, oldm) 25 | ``` 26 | 27 | ### How to use (specifics) 28 | There are a few basic assumptions for syntax: 29 | 1. Each %hook and %end directive is on a line by itself. 30 | 2. Function names within %hook blocks match the objective-C selector that they want to hook. 31 | 32 | This means that to hook SBApplicationController's method `- (void)_sendInstalledAppsDidChangeNotification:(id)arg1 removed:(id)arg2 modified:(id)arg3`, you would write the following script: 33 | ```javascript 34 | %hook SBApplicationController 35 | 36 | function _sendInstalledAppsDidChangeNotification:removed:modified:(arg1, arg2, arg3) { 37 | //Do stuff here 38 | %orig; 39 | } 40 | 41 | %end 42 | ``` 43 | 44 | To load the above script, install the loader tweak, save the script as a .cy file in `/Library/CyLogos/Tweaks` and run `killall SpringBoard` on your device. 45 | 46 | The syntax rules are pretty much the same as the actual Logos (no %hook nesting, %orig can be called with or without arguments, etc.). It's good practice to end each statement with a semicolon even though Cycript doesn't require it because it can sometimes mess up the Cycript parser if they're left out. 47 | 48 | ### FAQ 49 | 50 | __What can I use this for?__ 51 | 52 | This could be helpful for tweak developers to quickly prototype tweaks or for beginners to learn Logos syntax. 53 | 54 | __How do I use it?__ 55 | 56 | Write a tweak in cycript + logos syntax, drop it in `/Library/CyLogos/Tweaks` with the loader tweak installed, restart SpringBoard, and watch the system log for preprocessor or Cycript errors. See the examples folder for a few example tweaks (tested on iOS 6). 57 | 58 | __When will \_\_\_\_ be supported?__ 59 | 60 | It depends. Some things like C function hooking are on the to-do list (see `To do.txt`), but others like support for hooking daemons aren't. For use cases beyond basic tweaks or for more powerful objective-C features you should use [Theos](https://github.com/theos/theos). 61 | 62 | __Why is the preprocessor written in Python/why is \_\_\_\_ kind of hacky?__ 63 | 64 | A lot of this is kind of experimental/just me trying things out. Python is easy for prototyping and it conveniently runs on jailbroken iOS. I'll probably change the language used eventually. If you have a better way to do something, let me know! 65 | -------------------------------------------------------------------------------- /To do.txt: -------------------------------------------------------------------------------- 1 | To Do 2 | Add line numbers to all error output in cylogos.py 3 | Add support for filters (and apps other than springboard) (no filter = springboard maybe?) (maybe line starting with "###" at the top of the file) 4 | Change loader MS filter to load into all processes with UIKit instead of just springboard (once filters are implemented) 5 | Test on something more modern than iOS 6 6 | 7 | Maybe 8 | Add support for C function hooking? (%hookf) 9 | Support changes to files/re-hooking without killing the process (maybe not possible) 10 | Remove temporary files in destructor in loader? 11 | Support for reloading files when they're changed on disk (using file system events) (maybe not the best idea since people save frequently before they're done editing) 12 | Add option to show UIAlertView on error syntax/parsing error (for beginners) 13 | Rewrite parser in something other than python... (cycript?(lol) bash? perl?(ew)) -------------------------------------------------------------------------------- /cylogos.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import sys, string, re 3 | 4 | hookRegEx = re.compile(r'%hook\s+(\w+)\s*') 5 | functionRegEx = re.compile(r'\s*function\s+([\w|:]+)\s*\(([\w|,|\s]*)\)', re.DOTALL) 6 | 7 | def fatalError(message): 8 | print 'Error: ' + message 9 | exit(1) 10 | 11 | def assertValidKeyword(keyword, errorMsg, extraChars=[]): 12 | return 13 | # if not all(c in string.ascii_letters+string.digits+''.join(extraChars) for c in keyword): 14 | # fatalError(errorMsg) 15 | 16 | def lineIsHookStart(line): 17 | return line.strip()[:5].lower() == '%hook' 18 | 19 | def lineIsHookEnd(line): 20 | return line.strip().lower() == '%end' 21 | 22 | def getClassNameFromHook(line): 23 | match = hookRegEx.search(line) 24 | if match is None: 25 | fatalError('Incorrect hook syntax: ' + line) 26 | 27 | className = match.groups()[0] 28 | assertValidKeyword(className, 'Invalid class name in hook: ' + line) 29 | 30 | return className 31 | 32 | def processOrig(line, functionArgs, origMethodName): 33 | # No %orig calls to process 34 | if '%orig' not in line: 35 | return line 36 | 37 | # Process all %orig directives, ignoring them if they're inside string literals 38 | inDoubleQuoteString = False 39 | inSingleQuoteString = False 40 | for index, char in enumerate(line): 41 | if char == '"' and line[index-1] != '\\': 42 | inDoubleQuoteString = not inDoubleQuoteString 43 | elif char == '\'' and line[index-1] != '\\': 44 | inSingleQuoteString = not inSingleQuoteString 45 | 46 | # Found a valid %orig not in a string literal, process it 47 | if line[index:index+5] == '%orig' and not inDoubleQuoteString and not inSingleQuoteString: 48 | # Check for manual arguments to %orig 49 | origArguments = '' 50 | temp = line[index+5:].lstrip() 51 | if temp[0] == '(': 52 | origArguments = temp[1:temp.find(')')].strip() 53 | 54 | origCall = '->call(this)' 55 | lineEnd = line[index+5:] 56 | if len(origArguments) > 0: 57 | origCall = '->call(this, ' + origArguments + ')' 58 | lineEnd = line[line.find(')', index+5)+1:] 59 | elif len(functionArgs) > 0: 60 | origCall = '->call(this, ' + functionArgs + ')' 61 | line = line[:index] + origMethodName + origCall + lineEnd 62 | 63 | return line 64 | 65 | def parseFunctions(lines): 66 | parsedFunctions = [] 67 | 68 | # Join lines into one string 69 | line = '\n'.join(lines).rstrip() 70 | 71 | match = functionRegEx.search(line) 72 | while match is not None: 73 | # Get the body of the function (unfortunately can't do this with a regex) 74 | line = line[match.end():] 75 | line = line[line.find('{')+1:] 76 | bracketDepth = 1 77 | bodyEndIndex = 0 78 | for index, char in enumerate(line): 79 | if char == '{': 80 | bracketDepth += 1 81 | elif char == '}': 82 | bracketDepth -= 1 83 | 84 | if bracketDepth == 0: 85 | bodyEndIndex = index 86 | break 87 | 88 | functionBody = line[:bodyEndIndex] 89 | functionName, arguments = match.groups() 90 | parsedFunctions.append((functionName, arguments, functionBody)) 91 | 92 | line = line[bodyEndIndex+1:] 93 | match = functionRegEx.search(line) 94 | 95 | if len(line.strip()) > 0: 96 | fatalError('Only functions are allowed in hooks: ' + line) 97 | 98 | return parsedFunctions 99 | 100 | def processHook(lines): 101 | processedHookLines = [] 102 | className = getClassNameFromHook(lines[0]) 103 | lines = lines[1:-1] # remove %hook and %end lines 104 | 105 | funcs = parseFunctions(lines) 106 | 107 | for functionTuple in funcs: 108 | functionName = functionTuple[0] 109 | arguments = functionTuple[1] 110 | functionBody = functionTuple[2] 111 | 112 | # Create a variable for the old method implementation 113 | oldMethodName = className + '_' + functionName.replace(':', '_') + '_orig' 114 | processedHookLines.append('var ' + oldMethodName + ' = {};') 115 | 116 | processedHookLines.append('MS.hookMessage(' + className + ', @selector(' + functionName.replace('_', ':') + '), function(' + arguments + ') {') 117 | 118 | for functionBodyLine in functionBody.split('\n'): 119 | if len(functionBodyLine) > 0: 120 | processedHookLines.append(processOrig(functionBodyLine, arguments, oldMethodName)) 121 | 122 | processedHookLines.append('}, ' + oldMethodName + ')') 123 | 124 | return processedHookLines 125 | 126 | def main(): 127 | if len(sys.argv) < 2: 128 | fatalError('Need filename argument.') 129 | 130 | inHook = False 131 | processedLines = ['@import com.saurik.substrate.MS', '@import org.cycript.NSLog'] 132 | hookLines = [] 133 | 134 | file = open(sys.argv[1], 'r') 135 | for lineNum, line in enumerate(file): 136 | 137 | # Skip empty lines 138 | if len(line.strip()) == 0: 139 | continue 140 | 141 | line = line.rstrip() 142 | 143 | # Handle %hook 144 | if lineIsHookStart(line): 145 | if not inHook: 146 | inHook = True 147 | else: 148 | fatalError('Cannot nest %hook\'s (line ' + str(lineNum+1) + ').') 149 | 150 | # Process each line 151 | if inHook: 152 | hookLines.append(line) 153 | else: 154 | processedLines.append(line) 155 | 156 | # Handle %end 157 | if lineIsHookEnd(line): 158 | if inHook: 159 | inHook = False 160 | processedLines += processHook(hookLines) 161 | hookLines = [] 162 | else: 163 | fatalError('Found a %end without finding a %hook first (line ' + str(lineNum+1) + ').') 164 | 165 | if inHook: 166 | fatalError('Found a %hook without a matching %end.') 167 | 168 | file.close() 169 | 170 | for line in processedLines: 171 | print line 172 | 173 | if __name__ == "__main__": 174 | main() -------------------------------------------------------------------------------- /examples/DoubleHomeAlert.cy: -------------------------------------------------------------------------------- 1 | %hook SpringBoard 2 | 3 | function handleMenuDoubleTap() { 4 | var alert = [[UIAlertView alloc] initWithTitle:'Double Click' message:'You double clicked the home button.' delegate:nil cancelButtonTitle:'Ok' otherButtonTitles:nil]; 5 | [alert show]; 6 | } 7 | 8 | %end -------------------------------------------------------------------------------- /examples/PowerDownLogger.cy: -------------------------------------------------------------------------------- 1 | %hook SpringBoard 2 | 3 | function powerDownCanceled_(arg) { 4 | NSLog(@"Power down cancelled (from cycript) %@", arg); 5 | return %orig; 6 | } 7 | 8 | %end -------------------------------------------------------------------------------- /loader/CyLogos.plist: -------------------------------------------------------------------------------- 1 | { Filter = { Bundles = ( "com.apple.springboard" ); }; } 2 | -------------------------------------------------------------------------------- /loader/Makefile: -------------------------------------------------------------------------------- 1 | TARGET_IPHONEOS_DEPLOYMENT_VERSION = 6.0 2 | 3 | include $(THEOS)/makefiles/common.mk 4 | 5 | TWEAK_NAME = CyLogos 6 | CyLogos_FILES = Tweak.xm 7 | 8 | include $(THEOS_MAKE_PATH)/tweak.mk 9 | 10 | before-package:: 11 | cp -f ../cylogos.py layout/usr/bin/cylogos 12 | chmod +rx layout/usr/bin/cylogos 13 | 14 | after-install:: 15 | install.exec "killall -9 SpringBoard" 16 | -------------------------------------------------------------------------------- /loader/Tweak.xm: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | // NSString* const cyLogosDir = @"/Library/CyLogos"; 5 | NSString* const tweaksDir = @"/Library/CyLogos/Tweaks"; 6 | NSString* const preprocessedTempFile = @"/Library/CyLogos/tmp"; 7 | 8 | NSString* runCommand(NSString *command, int* returnValue) { 9 | FILE *fp = popen([command UTF8String], "r"); 10 | if (fp == NULL) { 11 | *returnValue = -1; 12 | return @"Error running command."; 13 | } 14 | 15 | NSFileHandle *fileHandle = [[NSFileHandle alloc] initWithFileDescriptor:fileno(fp)]; 16 | NSString *output = [[NSString alloc] initWithData:[fileHandle availableData] encoding:NSUTF8StringEncoding]; 17 | 18 | *returnValue = pclose(fp); 19 | return output; 20 | } 21 | 22 | void subscribeToFSNotifications(NSString* file, void(^changeBlock)(unsigned long)) { 23 | __block int fileDescriptor = open([file UTF8String], O_EVTONLY); 24 | dispatch_queue_t defaultQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 25 | dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, fileDescriptor, DISPATCH_VNODE_DELETE | DISPATCH_VNODE_WRITE, defaultQueue); 26 | 27 | dispatch_source_set_event_handler(source, ^{ 28 | unsigned long eventTypes = dispatch_source_get_data(source); 29 | changeBlock(eventTypes); 30 | }); 31 | 32 | dispatch_source_set_cancel_handler(source, ^{ 33 | close(fileDescriptor); 34 | fileDescriptor = 0; 35 | }); 36 | 37 | dispatch_resume(source); 38 | } 39 | 40 | void injectScript(NSString *fileName) { 41 | NSLog(@"CyLogos loader: Injecting %@", fileName); 42 | 43 | int returnValue = 0; 44 | NSString *output = @""; 45 | 46 | //Preprocess with CyLogos 47 | output = runCommand([NSString stringWithFormat:@"cylogos %@/%@ > %@", tweaksDir, fileName, preprocessedTempFile], &returnValue); 48 | if (returnValue != 0) { 49 | output = runCommand([NSString stringWithFormat:@"cat %@", preprocessedTempFile], &returnValue); 50 | NSLog(@"CyLogos loader: Error preprocessing %@: %@", fileName, output); 51 | return; 52 | } 53 | 54 | //Compile with Cycript 55 | NSString *tempFile = runCommand(@"mktemp -t cylogos.XXXXX", &returnValue); 56 | if (returnValue != 0) { 57 | NSLog(@"CyLogos loader: Error obtaining temporary file."); 58 | return; 59 | } 60 | output = runCommand([NSString stringWithFormat:@"cycript -c %@ > %@", preprocessedTempFile, tempFile], &returnValue); 61 | if (returnValue != 0) { 62 | output = runCommand([NSString stringWithFormat:@"cat %@", tempFile], &returnValue); 63 | NSLog(@"CyLogos loader: Error compiling %@: %@", fileName, output); 64 | return; 65 | } 66 | 67 | //Inject with Cycript 68 | NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"]; 69 | output = runCommand([NSString stringWithFormat:@"cycript -p %@ %@", appName, tempFile], &returnValue); //This almost always segfaults cycript (at least on iOS 6), idk why. Script is still injected though 70 | 71 | NSLog(@"CyLogos loader: Successfully injected %@", fileName); 72 | } 73 | 74 | %ctor { 75 | NSLog(@"CyLogos loader is loaded! (bundle ID: %@)", [[NSBundle mainBundle] bundleIdentifier]); 76 | 77 | // subscribeToFSNotifications(tweaksDir, ^void(unsigned long eventTypes) { 78 | // NSLog(@"STUFF CHANGED IN TWEAKS DIR"); 79 | // if (eventTypes & DISPATCH_VNODE_DELETE) 80 | // NSLog(@"deleted"); 81 | // if (eventTypes & DISPATCH_VNODE_WRITE) 82 | // NSLog(@"modified"); 83 | 84 | // loadScripts(); 85 | // }); 86 | 87 | // CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), NULL, (CFNotificationCallback)loadScripts, CFSTR("me.thomasfinch.cylogos-reload"), NULL, CFNotificationSuspensionBehaviorDeliverImmediately); 88 | 89 | NSArray* cyFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:tweaksDir error:NULL]; 90 | for (NSString* file in cyFiles) { 91 | // To do: Check the filter on each file to make sure it matches the current process bundle ID 92 | injectScript(file); 93 | } 94 | } 95 | 96 | %dtor { 97 | NSLog(@"CYLOGOS LOADER IS DESTRUCTING!!!! ASDFASDFASDFASDFASDFASDFASDFDS"); 98 | //Remove owned temp files here. Unfortunately this isn't called when springboard is killed forcibly. 99 | } 100 | -------------------------------------------------------------------------------- /loader/layout/DEBIAN/control: -------------------------------------------------------------------------------- 1 | Package: me.thomasfinch.cylogos 2 | Name: CyLogos 3 | Depends: mobilesubstrate 4 | Version: 0.1 5 | Architecture: iphoneos-arm 6 | Description: Prototype tweaks in Cycript using Logos syntax 7 | Maintainer: Thomas Finch 8 | Author: Thomas Finch 9 | Section: Tweaks 10 | -------------------------------------------------------------------------------- /loader/layout/usr/bin/cylogos: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import sys, string 3 | 4 | def fatalError(message): 5 | print 'Error: ' + message 6 | exit(1) 7 | 8 | def assertValidKeyword(keyword, errorMsg, extraChars=[]): 9 | if not all(c in string.ascii_letters+string.digits+''.join(extraChars) for c in keyword): 10 | fatalError(errorMsg) 11 | 12 | def lineIsHookStart(line): 13 | return line[:5].lower() == '%hook' 14 | 15 | def lineIsHookEnd(line): 16 | return line.lower() == '%end' 17 | 18 | def getClassNameFromHook(line): 19 | line = line[5:].strip() # remove '%hook' 20 | 21 | if len(line) == 0: 22 | fatalError('Missing class name in hook.') 23 | 24 | assertValidKeyword(line, 'Invalid class name in hook: ' + line) 25 | 26 | return line 27 | 28 | def processOrig(line, functionArgs, oldMethodName): 29 | # No %orig calls to process 30 | if '%orig' not in line: 31 | return line 32 | 33 | # Process all %orig directives, ignoring them if they're inside string literals 34 | inDoubleQuoteString = False 35 | inSingleQuoteString = False 36 | for index, char in enumerate(line): 37 | if char == '"' and line[index-1] != '\\': 38 | inDoubleQuoteString = not inDoubleQuoteString 39 | elif char == '\'' and line[index-1] != '\\': 40 | inSingleQuoteString = not inSingleQuoteString 41 | 42 | # Found a valid %orig not in a string literal, process it 43 | if line[index:index+5] == '%orig' and not inDoubleQuoteString and not inSingleQuoteString: 44 | # if line[index+6] == '(': 45 | # line = line # TEMPORARY CHANGE THIS ASDFADSF ASDF ASDF 46 | # else: 47 | 48 | origCall = '->call(this)' 49 | if len(functionArgs) > 0: 50 | origCall = '->call(this, ' + functionArgs + ')' 51 | line = line[:index] + oldMethodName + origCall + line[index+5:] 52 | 53 | return line 54 | 55 | def parseFunctions(lines): 56 | parsedFunctions = [] 57 | 58 | # Join lines into one string 59 | line = '\n'.join(lines).rstrip() 60 | 61 | while len(line) > 0: 62 | # remove 'function' 63 | line = line.lstrip() 64 | if line[:8].lower() != 'function': 65 | fatalError('Only functions are allowed within hooks.') 66 | line = line[8:].lstrip() 67 | 68 | # get function name 69 | functionName = line[:line.find('(')].strip() 70 | assertValidKeyword(functionName, 'Invalid function name: ' + functionName, ['_']) 71 | line = line[line.find('('):].lstrip() 72 | 73 | # get arguments 74 | arguments = line[1:line.find(')')].strip() 75 | assertValidKeyword(arguments, 'Invalid function arguments: ' + arguments, [',', ' ']) 76 | line = line[line.find(')')+1:].lstrip() 77 | 78 | # get function body 79 | if line[0] != '{': 80 | fatalError('Missing function body.') 81 | bracketDepth = 0 82 | functionEndIndex = 0 83 | for index, char in enumerate(line): 84 | if char == '{': 85 | bracketDepth += 1 86 | elif char == '}': 87 | bracketDepth -= 1 88 | 89 | if bracketDepth == 0: 90 | functionEndIndex = index 91 | break 92 | 93 | functionBody = line[1:functionEndIndex].strip() 94 | line = line[functionEndIndex+1:].lstrip() 95 | 96 | parsedFunctions.append((functionName, arguments, functionBody)) 97 | 98 | return parsedFunctions 99 | 100 | def processHook(lines): 101 | processedHookLines = [] 102 | className = getClassNameFromHook(lines[0]) 103 | lines = lines[1:-1] # remove %hook and %end lines 104 | 105 | funcs = parseFunctions(lines) 106 | 107 | for functionTuple in funcs: 108 | functionName = functionTuple[0] 109 | arguments = functionTuple[1] 110 | functionBody = functionTuple[2] 111 | 112 | # Create a variable for the old method implementation 113 | oldMethodName = className + '_' + functionName + '_orig' 114 | processedHookLines.append('var ' + oldMethodName + ' = {};') 115 | 116 | processedHookLines.append('MS.hookMessage(' + className + ', @selector(' + functionName.replace('_', ':') + '), function(' + arguments + ') {') 117 | 118 | for functionBodyLine in functionBody.split('\n'): 119 | processedHookLines.append(processOrig(functionBodyLine, arguments, oldMethodName)) 120 | 121 | processedHookLines.append('}, ' + oldMethodName + ')') 122 | 123 | return processedHookLines 124 | 125 | def main(): 126 | if len(sys.argv) < 2: 127 | fatalError('Need filename argument.') 128 | 129 | inHook = False 130 | processedLines = ['@import com.saurik.substrate.MS', '@import org.cycript.NSLog'] 131 | hookLines = [] 132 | 133 | file = open(sys.argv[1], 'r') 134 | for lineNum, line in enumerate(file): 135 | 136 | # Clean up input (strip whitespace, skip empty lines) 137 | line = line.strip() 138 | if len(line) == 0: 139 | continue 140 | 141 | # Handle %hook 142 | if lineIsHookStart(line): 143 | if not inHook: 144 | inHook = True 145 | else: 146 | fatalError('Cannot nest %hook\'s (line ' + str(lineNum+1) + ').') 147 | 148 | # Process each line 149 | if inHook: 150 | hookLines.append(line) 151 | else: 152 | processedLines.append(line) 153 | 154 | # Handle %end 155 | if lineIsHookEnd(line): 156 | if inHook: 157 | inHook = False 158 | processedLines += processHook(hookLines) 159 | hookLines = [] 160 | else: 161 | fatalError('Found a %end without finding a %hook first (line ' + str(lineNum+1) + ').') 162 | 163 | if inHook: 164 | fatalError('Found a %hook without a matching %end.') 165 | 166 | file.close() 167 | 168 | for line in processedLines: 169 | print line 170 | 171 | if __name__ == "__main__": 172 | main() -------------------------------------------------------------------------------- /oldVersion.py: -------------------------------------------------------------------------------- 1 | import sys, re, string 2 | 3 | hookRegEx = re.compile(r'%hook\s+(\w+)\s*\n\s*(.*?)\n\s*%end', re.DOTALL) 4 | functionRegEx = re.compile(r'\s*function\s+([\w|:]+)\s*\(([\w|,|\s]*)\)', re.DOTALL) 5 | origWithArgsRegEx = re.compile(r'%orig\s*\(([\w|,|\s]*)\)') 6 | 7 | def fatalError(message): 8 | print 'Error: ' + message 9 | exit(1) 10 | 11 | def processOrig(body, origMethodName, functionArgs): 12 | # Process all calls to %orig with arguments 13 | match = origWithArgsRegEx.search(body) 14 | while match is not None: 15 | arguments = match.groups()[0] 16 | body = body[:match.start()] + origMethodName + '->call(this, ' + arguments + ')' + body[match.end():] 17 | match = origWithArgsRegEx.search(body) 18 | 19 | # Process all calls to %orig without arguments 20 | while body.find('%orig') != -1: 21 | origIndex = body.find('%orig') 22 | body = body[:origIndex] + origMethodName + '->call(this)' + body[origIndex+5:] 23 | 24 | return body 25 | 26 | def processFunction(regExMatch, hookedClass, functionBody): 27 | functionName, arguments = regExMatch.groups() 28 | 29 | # check that the number of :'s == number of function arguments 30 | 31 | origMethodName = hookedClass + '_' + functionName.replace(':', '_') + '_orig' 32 | functionBody = processOrig(functionBody, origMethodName, arguments) 33 | 34 | returnStr = 'var ' + origMethodName + ' = {};\n' 35 | returnStr += 'MS.hookMessage(' + hookedClass + ', @selector(' + functionName + '), function(' + arguments + ') {' 36 | returnStr += functionBody 37 | returnStr += '}, ' + origMethodName + ')\n\n' 38 | 39 | return returnStr 40 | 41 | def processHook(regExMatch): 42 | hookedClass, hookBody = regExMatch.groups() 43 | 44 | # Check that hookBody doesn't contain any %hook's (can't be nested) 45 | if '%hook' in hookBody: 46 | fatalError('Cannot nest %hook\'s') 47 | 48 | processedStr = '' 49 | match = functionRegEx.search(hookBody) 50 | while match is not None: 51 | processedStr += hookBody[:match.start()] 52 | 53 | # Get the body of the function (unfortunately can't do this with a regex) 54 | hookBody = hookBody[match.end():] 55 | hookBody = hookBody[hookBody.find('{')+1:] 56 | bracketDepth = 1 57 | bodyEndIndex = 0 58 | for index, char in enumerate(hookBody): 59 | if char == '{': 60 | bracketDepth += 1 61 | elif char == '}': 62 | bracketDepth -= 1 63 | 64 | if bracketDepth == 0: 65 | bodyEndIndex = index 66 | break 67 | 68 | functionBody = hookBody[:bodyEndIndex] 69 | hookBody = hookBody[bodyEndIndex+1:] 70 | processedStr += processFunction(match, hookedClass, functionBody) 71 | match = functionRegEx.search(hookBody) 72 | 73 | return processedStr 74 | 75 | 76 | def main(): 77 | if len(sys.argv) < 2: 78 | fatalError('Need filename argument.') 79 | 80 | file = open(sys.argv[1], 'r') 81 | fileStr = file.read() 82 | 83 | # Find and process all %hook...%end blocks 84 | match = hookRegEx.search(fileStr) 85 | while match is not None: 86 | fileStr = fileStr[:match.start()] + processHook(match) + fileStr[match.end():] 87 | match = hookRegEx.search(fileStr) 88 | 89 | print fileStr 90 | 91 | # Check that there aren't any remaining %hook's or %end's, if so then there's a mismatch 92 | if fileStr.find('%hook') != -1: 93 | fatalError('Found %hook without a %end.') 94 | if '%end' in fileStr: 95 | fatalError('Found %end without a %hook.') 96 | 97 | fileStr = '@import com.saurik.substrate.MS\n@import org.cycript.NSLog\n\n' + fileStr 98 | 99 | print fileStr 100 | 101 | if __name__ == "__main__": 102 | main() 103 | -------------------------------------------------------------------------------- /testCases/invalidTests/test.cy: -------------------------------------------------------------------------------- 1 | // function keyword is messed up 2 | 3 | %hook NSObject 4 | 5 | func,tion description() { 6 | return %orig + " (of doom)"; 7 | } 8 | 9 | %end -------------------------------------------------------------------------------- /testCases/invalidTests/test2.cy: -------------------------------------------------------------------------------- 1 | //Test %hook without a %end 2 | 3 | %hook NSObject 4 | 5 | function declaration() { 6 | return 'testing!'; 7 | } -------------------------------------------------------------------------------- /testCases/invalidTests/test3.cy: -------------------------------------------------------------------------------- 1 | //Test nested %hooks 2 | 3 | %hook NSObject 4 | 5 | function declaration() { 6 | return 'testing!'; 7 | } 8 | 9 | %hook UIView 10 | 11 | %end -------------------------------------------------------------------------------- /testCases/invalidTests/test4.cy: -------------------------------------------------------------------------------- 1 | //Test dangling %end 2 | 3 | %hook NSObject 4 | 5 | function declaration() { 6 | return 'testing!'; 7 | } 8 | 9 | %end 10 | 11 | %end -------------------------------------------------------------------------------- /testCases/runTests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | passedAllTests=true 4 | 5 | # Run all valid tests, make sure they return 0 6 | for i in $(ls validTests); do 7 | python ../cylogos.py validTests/$i > /dev/null 8 | if [ $? -ne 0 ] 9 | then 10 | echo 'Valid test case failed:' $i 11 | passedAllTests=false 12 | fi 13 | done 14 | 15 | # Run all invalid tests, make sure they don't return 0 16 | for i in $(ls invalidTests); do 17 | python ../cylogos.py invalidTests/$i > /dev/null 18 | if [ $? -eq 0 ] 19 | then 20 | echo 'Invalid test case failed:' $i 21 | passedAllTests=false 22 | fi 23 | done 24 | 25 | 26 | if [ "$passedAllTests" = true ] ; then 27 | echo 'All tests passed' 28 | fi -------------------------------------------------------------------------------- /testCases/validTests/test.cy: -------------------------------------------------------------------------------- 1 | // Example from the cycript manual, in logos 2 | 3 | %hook NSObject 4 | 5 | function description() { 6 | return %orig + " (of doom)"; 7 | } 8 | 9 | %end 10 | 11 | printf('%s\n', [[[[NSObject alloc] init] description] UTF8String]); -------------------------------------------------------------------------------- /testCases/validTests/test2.cy: -------------------------------------------------------------------------------- 1 | // Tests excessive whitespace (spaces, tabs, and newlines) in %hook, %end, and function definitions 2 | 3 | 4 | %hook NSObject 5 | 6 | function 7 | 8 | 9 | 10 | testing 11 | 12 | 13 | 14 | ( ) { 15 | 16 | 17 | 18 | printf("testing\n") 19 | 20 | 21 | } 22 | 23 | %end -------------------------------------------------------------------------------- /testCases/validTests/test3.cy: -------------------------------------------------------------------------------- 1 | // Idk just a general valid test case 2 | 3 | %hook NSObject 4 | 5 | function testing() { 6 | printf("testing\n") 7 | } 8 | 9 | function asdf(arg1, arg2, thirdArg) { 10 | var a = 5 11 | var b = 3 * 10 12 | if (a >= 5) { 13 | a++ 14 | } 15 | return a + b 16 | } 17 | 18 | %end -------------------------------------------------------------------------------- /testCases/validTests/test4.cy: -------------------------------------------------------------------------------- 1 | // Tests one line functions 2 | 3 | %hook NSObject 4 | 5 | function testing() { printf("testing\n") } 6 | 7 | %end -------------------------------------------------------------------------------- /testCases/validTests/test5.cy: -------------------------------------------------------------------------------- 1 | // General test case (tests functions with arguments) 2 | 3 | %hook NSArray 4 | 5 | function writeToFile_atomically_(file, atomically) { 6 | printf('Writing to file atomically!\n'); 7 | printf('Filename: %s\n', [file UTF8String]); 8 | printf('Atomically: %d\n', atomically); 9 | return YES; 10 | } 11 | 12 | %end 13 | 14 | 15 | var arr = [[NSArray alloc] init]; 16 | [arr writeToFile:@"testFile" atomically:YES]; --------------------------------------------------------------------------------