diff options
| author | Jordan Wiens <jordan@psifertex.com> | 2019-09-03 15:10:17 -0400 |
|---|---|---|
| committer | Jordan Wiens <jordan@psifertex.com> | 2019-09-03 15:10:17 -0400 |
| commit | e66693398775137273694ba394cd49dea8ae8981 (patch) | |
| tree | 00fb56565bfbfee327fcc155ce2e4d5c99b0f6f5 | |
| parent | 1ae0f8b0ad13e69835818d245745e8377ec86376 (diff) | |
modifying rlcompleter to support completion through list lookups, updating license information as well
| -rw-r--r-- | docs/about/open-source.md | 4 | ||||
| -rw-r--r-- | python/bncompleter.py | 197 | ||||
| -rw-r--r-- | python/scriptingprovider.py | 6 |
3 files changed, 204 insertions, 3 deletions
diff --git a/docs/about/open-source.md b/docs/about/open-source.md index d9384ab2..a90da9c1 100644 --- a/docs/about/open-source.md +++ b/docs/about/open-source.md @@ -19,6 +19,7 @@ The previous tools are used in the generation of our documentation, but are not - [opensans] ([opensans license] - Apache 2.0) - [qt] ([qt license] - LGPLv3 / note, please see our [qt build instructions below](open-source.md#building-qt)) - [sourcecodepro] ([sourcecodepro license] - SIL open font license) + - [rlcompleter] ([python license] - Python Software Foundation License 2) * Core - [discount] ([discount license] - BSD) @@ -26,6 +27,7 @@ The previous tools are used in the generation of our documentation, but are not - [libmspack] ([libmspack license] - LGPL, v2) - [llvm] ([llvm license] - BSD-style) - [lzf] ([lzf license] - BSD) + - [python] ([python license] - Python Software Foundation License 2 -- a Python 2.7 built without GPL components is currently shipped with Windows builds only, other platforms leverage existing Python installs) - [sqlite] ([sqlite license] - public domain) - [zlib] ([zlib license] - zlib license) @@ -82,8 +84,10 @@ Please note that we offer no support for running Binary Ninja with modified Qt l [opensans license]: http://www.apache.org/licenses/LICENSE-2.0.html [opensans]: https://www.google.com/fonts/specimen/Open+Sans [PPC architecture module]: https://github.com/Vector35/ppc-capstone +[python license]: https://github.com/python/cpython/blob/master/LICENSE [qt license]: https://www.qt.io/qt-licensing-terms/ [qt]: https://www.qt.io/download/ +[rlcompleter]: https://github.com/python/cpython/blob/master/Lib/rlcompleter.py [sourcecodepro license]: https://github.com/adobe-fonts/source-code-pro/blob/master/LICENSE.md [sourcecodepro]: https://github.com/adobe-fonts/source-code-pro [sphinx license]: https://github.com/sphinx-doc/sphinx/blob/master/LICENSE diff --git a/python/bncompleter.py b/python/bncompleter.py new file mode 100644 index 00000000..b80cabca --- /dev/null +++ b/python/bncompleter.py @@ -0,0 +1,197 @@ +""" This file is a modified version of rlcompleter.py from the Python +project under the Python Software Foundation License 2: +https://github.com/python/cpython/blob/master/Lib/rlcompleter.py +https://github.com/python/cpython/blob/master/LICENSE + +The only changes made were to modify the regular expression in attr_matches +and all code that relied on GNU readline (the later more for readability as +it wasn't required). +-------------- +Word completion for GNU readline. + +The completer completes keywords, built-ins and globals in a selectable +namespace (which defaults to __main__); when completing NAME.NAME..., it +evaluates (!) the expression up to the last dot and completes its attributes. + +It's very cool to do "import sys" type "sys.", hit the completion key (twice), +and see the list of names defined by the sys module! + +Tip: to use the tab key as the completion key, call + + readline.parse_and_bind("tab: complete") + +Notes: + +- Exceptions raised by the completer function are *ignored* (and generally cause + the completion to fail). This is a feature -- since readline sets the tty + device in raw (or cbreak) mode, printing a traceback wouldn't work well + without some complicated hoopla to save, reset and restore the tty state. + +- The evaluation of the NAME.NAME... form may cause arbitrary application + defined code to be executed if an object with a __getattr__ hook is found. + Since it is the responsibility of the application (or the user) to enable this + feature, I consider this an acceptable risk. More complicated expressions + (e.g. function calls or indexing operations) are *not* evaluated. + +- When the original stdin is not a tty device, GNU readline is never + used, and this module (and the readline module) are silently inactive. + +""" + +import atexit +import builtins +import __main__ + +__all__ = ["Completer"] + +class Completer: + def __init__(self, namespace = None): + """Create a new completer for the command line. + + Completer([namespace]) -> completer instance. + + If unspecified, the default namespace where completions are performed + is __main__ (technically, __main__.__dict__). Namespaces should be + given as dictionaries. + + Completer instances should be used as the completion mechanism of + readline via the set_completer() call: + + readline.set_completer(Completer(my_namespace).complete) + """ + + if namespace and not isinstance(namespace, dict): + raise TypeError('namespace must be a dictionary') + + # Don't bind to namespace quite yet, but flag whether the user wants a + # specific namespace or to use __main__.__dict__. This will allow us + # to bind to __main__.__dict__ at completion time, not now. + if namespace is None: + self.use_main_ns = 1 + else: + self.use_main_ns = 0 + self.namespace = namespace + + def complete(self, text, state): + """Return the next possible completion for 'text'. + + This is called successively with state == 0, 1, 2, ... until it + returns None. The completion should begin with 'text'. + + """ + if self.use_main_ns: + self.namespace = __main__.__dict__ + + if not text.strip(): + if state == 0: + return '\t' + else: + return None + + if state == 0: + if "." in text: + self.matches = self.attr_matches(text) + else: + self.matches = self.global_matches(text) + try: + return self.matches[state] + except IndexError: + return None + + def _callable_postfix(self, val, word): + if callable(val): + word = word + "(" + return word + + def global_matches(self, text): + """Compute matches when text is a simple name. + + Return a list of all keywords, built-in functions and names currently + defined in self.namespace that match. + + """ + import keyword + matches = [] + seen = {"__builtins__"} + n = len(text) + for word in keyword.kwlist: + if word[:n] == text: + seen.add(word) + if word in {'finally', 'try'}: + word = word + ':' + elif word not in {'False', 'None', 'True', + 'break', 'continue', 'pass', + 'else'}: + word = word + ' ' + matches.append(word) + for nspace in [self.namespace, builtins.__dict__]: + for word, val in nspace.items(): + if word[:n] == text and word not in seen: + seen.add(word) + matches.append(self._callable_postfix(val, word)) + return matches + + def attr_matches(self, text): + """Compute matches when text contains a dot. + + Assuming the text is of the form NAME.NAME....[NAME], and is + evaluable in self.namespace, it will be evaluated and its attributes + (as revealed by dir()) are used as possible completions. (For class + instances, class members are also considered.) + + WARNING: this can still invoke arbitrary C code, if an object + with a __getattr__ hook is evaluated. + + """ + import re + m = re.match(r"([\w\[\]]+(\.[\w\[\]]+)*)\.(\w*)", text) + if not m: + return [] + expr, attr = m.group(1, 3) + try: + thisobject = eval(expr, self.namespace) + except Exception: + return [] + + # get the content of the object, except __builtins__ + words = set(dir(thisobject)) + words.discard("__builtins__") + + if hasattr(thisobject, '__class__'): + words.add('__class__') + words.update(get_class_members(thisobject.__class__)) + matches = [] + n = len(attr) + if attr == '': + noprefix = '_' + elif attr == '_': + noprefix = '__' + else: + noprefix = None + while True: + for word in words: + if (word[:n] == attr and + not (noprefix and word[:n+1] == noprefix)): + match = "%s.%s" % (expr, word) + try: + val = getattr(thisobject, word) + except Exception: + pass # Include even if attribute not set + else: + match = self._callable_postfix(val, match) + matches.append(match) + if matches or not noprefix: + break + if noprefix == '_': + noprefix = '__' + else: + noprefix = None + matches.sort() + return matches + +def get_class_members(klass): + ret = dir(klass) + if hasattr(klass,'__bases__'): + for base in klass.__bases__: + ret = ret + get_class_members(base) + return ret
\ No newline at end of file diff --git a/python/scriptingprovider.py b/python/scriptingprovider.py index b1405790..b9969dd2 100644 --- a/python/scriptingprovider.py +++ b/python/scriptingprovider.py @@ -25,7 +25,7 @@ import ctypes import threading import abc import sys -import rlcompleter +from binaryninja import bncompleter import re # Binary Ninja components @@ -118,7 +118,7 @@ class ScriptingInstance(object): self._cb.completeInput = self._cb.completeInput.__class__(self._complete_input) self._cb.completeInput.restype = ctypes.c_void_p self.handle = core.BNInitScriptingInstance(provider.handle, self._cb) - self.delimiters = ' \t\n`~!@#$%^&*()-=+[{]}\\|;:\'",<>/?' + self.delimiters = ' \t\n`~!@#$%^&*()-=+{}\\|;:\'",<>/?' else: self.handle = core.handle_of_type(handle, core.BNScriptingInstance) self.listeners = [] @@ -536,7 +536,7 @@ class PythonScriptingInstance(ScriptingInstance): self.code = None self.input = "" - self.completer = rlcompleter.Completer(namespace = self.locals) + self.completer = bncompleter.Completer(namespace = self.locals) self.interpreter.push("from binaryninja import *") |
