summaryrefslogtreecommitdiff
path: root/python/examples/snippets/__init__.py
diff options
context:
space:
mode:
authorJordan Wiens <jordan@psifertex.com>2019-03-15 21:14:05 -0400
committerRusty Wagner <rusty@vector35.com>2019-03-20 13:00:17 -0400
commit8ab595cfbf92b80d4ef81913d0c82621db114702 (patch)
treeef036c49027340c16344fec11f1d26f2ecba536d /python/examples/snippets/__init__.py
parent755bfb43cc5cda7e9a34027e2b6d2a00f073379b (diff)
initial snippets plugin
Diffstat (limited to 'python/examples/snippets/__init__.py')
-rw-r--r--python/examples/snippets/__init__.py301
1 files changed, 301 insertions, 0 deletions
diff --git a/python/examples/snippets/__init__.py b/python/examples/snippets/__init__.py
new file mode 100644
index 00000000..4b5fb13f
--- /dev/null
+++ b/python/examples/snippets/__init__.py
@@ -0,0 +1,301 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+import sys
+import os
+import re
+from PySide2.QtWidgets import (QLineEdit, QPushButton, QApplication, QTextEdit, QWidget,
+ QVBoxLayout, QHBoxLayout, QDialog, QFileSystemModel, QTreeView, QLabel, QSplitter,
+ QInputDialog, QMessageBox, QHeaderView, QMenu, QAction, QKeySequenceEdit,
+ QPlainTextEdit)
+from PySide2.QtCore import (QDir, QObject, Qt, QFileInfo, QItemSelectionModel, QSettings)
+from PySide2.QtGui import (QFont, QFontMetrics, QDesktopServices, QKeySequence)
+from binaryninja import user_plugin_path
+from binaryninja.plugin import PluginCommand, MainThreadActionHandler
+from binaryninja.mainthread import execute_on_main_thread
+from binaryninja.log import (log_info, log_warn, log_alert, log_debug)
+from binaryninjaui import (getMonospaceFont, UIAction, UIActionHandler)
+
+snippetPath = os.path.realpath(os.path.join(user_plugin_path(), "..", "snippets"))
+
+def includeWalk(dir, includeExt):
+ filePaths = []
+ for (root, dirs, files) in os.walk(dir):
+ for f in files:
+ if os.path.splitext(f)[1] in includeExt:
+ filePaths.append(os.path.join(root, f))
+ return filePaths
+
+def loadSnippetFromFile(snippetPath):
+ try:
+ snippetText = open(snippetPath, 'r').readlines()
+ except:
+ return (False, [], False)
+ if (len(snippetText) < 3):
+ return (False, [], False)
+ else:
+ qKeySequence = QKeySequence(snippetText[1].strip()[1:])
+ if qKeySequence.isEmpty():
+ qKeySequence = []
+ else:
+ qKeySequence = [qKeySequence]
+ return (snippetText[0].strip()[1:],
+ qKeySequence,
+ ''.join(snippetText[2:])
+ )
+
+class Snippets(QDialog):
+
+ def __init__(self, parent=None):
+ super(Snippets, self).__init__(parent)
+ # Create widgets
+ self.setWindowModality(Qt.NonModal)
+ self.title = QLabel(self.tr("Snippet Editor"))
+ self.saveButton = QPushButton(self.tr("Save"))
+ self.revertButton = QPushButton(self.tr("Revert"))
+ self.clearHotkeyButton = QPushButton(self.tr("Clear Hotkey"))
+ self.setWindowTitle(self.title.text())
+ self.newFolderButton = QPushButton("New Folder")
+ self.deleteSnippetButton = QPushButton("Delete")
+ self.newSnippetButton = QPushButton("New Snippet")
+ self.edit = QPlainTextEdit()
+ self.resetting = False
+ self.columns = 3
+
+ self.keySequenceEdit = QKeySequenceEdit(self)
+ self.currentHotkey = QKeySequence()
+ self.currentHotkeyLabel = QLabel("")
+ self.currentFileLabel = QLabel()
+ self.currentFile = ""
+ self.snippetDescription = QLineEdit()
+ self.snippetEditsPending = False
+
+ self.clearSelection()
+
+ #Set Editbox Size
+ font = getMonospaceFont(self)
+ self.edit.setFont(font)
+ font = QFontMetrics(font)
+ self.edit.setTabStopWidth(4 * font.width(' ')); #TODO, replace with settings API
+
+ #Files
+ self.files = QFileSystemModel()
+ self.files.setRootPath(snippetPath)
+ self.files.setNameFilters(["*.py"])
+
+ #Tree
+ self.tree = QTreeView()
+ self.tree.setModel(self.files)
+ self.tree.setSortingEnabled(True)
+ self.tree.hideColumn(2)
+ self.tree.sortByColumn(0, Qt.AscendingOrder)
+ self.tree.setRootIndex(self.files.index(snippetPath))
+ for x in range(self.columns):
+ #self.tree.resizeColumnToContents(x)
+ self.tree.header().setSectionResizeMode(x, QHeaderView.ResizeToContents)
+ treeLayout = QVBoxLayout()
+ treeLayout.addWidget(self.tree)
+ treeButtons = QHBoxLayout()
+ treeButtons.addWidget(self.newFolderButton)
+ treeButtons.addWidget(self.newSnippetButton)
+ treeButtons.addWidget(self.deleteSnippetButton)
+ treeLayout.addLayout(treeButtons)
+ treeWidget = QWidget()
+ treeWidget.setLayout(treeLayout)
+
+ # Create layout and add widgets
+ buttons = QHBoxLayout()
+ buttons.addWidget(self.clearHotkeyButton)
+ buttons.addWidget(self.keySequenceEdit)
+ buttons.addWidget(self.currentHotkeyLabel)
+ buttons.addWidget(self.revertButton)
+ buttons.addWidget(self.saveButton)
+
+ description = QHBoxLayout()
+ description.addWidget(QLabel(self.tr("Description: ")))
+ description.addWidget(self.snippetDescription)
+
+ vlayoutWidget = QWidget()
+ vlayout = QVBoxLayout()
+ vlayout.addWidget(self.currentFileLabel)
+ vlayout.addWidget(self.edit)
+ vlayout.addLayout(description)
+ vlayout.addLayout(buttons)
+ vlayoutWidget.setLayout(vlayout)
+
+ hsplitter = QSplitter()
+ hsplitter.addWidget(treeWidget)
+ hsplitter.addWidget(vlayoutWidget)
+
+ hlayout = QHBoxLayout()
+ hlayout.addWidget(hsplitter)
+
+ self.showNormal() #Fixes bug that maximized windows are "stuck"
+ self.settings = QSettings("Vector35", "Snippet Editor")
+ if self.settings.contains("ui/snippeteditor/geometry"):
+ self.restoreGeometry(self.settings.value("ui/snippeteditor/geometry"))
+ else:
+ self.edit.setMinimumWidth(80 * font.averageCharWidth())
+ self.edit.setMinimumHeight(30 * font.lineSpacing())
+
+ # Set dialog layout
+ self.setLayout(hlayout)
+
+ # Add signals
+ self.saveButton.clicked.connect(self.save)
+ self.revertButton.clicked.connect(self.loadSnippet)
+ self.clearHotkeyButton.clicked.connect(self.clearHotkey)
+ self.tree.selectionModel().selectionChanged.connect(self.selectFile)
+ self.newSnippetButton.clicked.connect(self.newFileDialog)
+ self.deleteSnippetButton.clicked.connect(self.deleteSnippet)
+ self.newFolderButton.clicked.connect(self.newFolder)
+
+ def executeSnippet(self, code, context):
+ snippetGlobals = {}
+ snippetGlobals['current_view'] = context.binaryView
+ snippetGlobals['bv'] = context.binaryView
+ snippetGlobals['current_function'] = context.function
+ #snippetGlobals['current_basic_block'] = context.block
+ snippetGlobals['current_address'] = context.address
+ snippetGlobals['here'] = context.address
+ snippetGlobals['current_selection'] = (context.address, context.address+context.length)
+ snippetGlobals['current_llil'] = context.lowLevelILFunction
+ snippetGlobals['current_mlil'] = context.mediumLevelILFunction
+
+ exec("from binaryninja import *", snippetGlobals)
+ exec(code, snippetGlobals)
+ if snippetGlobals['here'] != context.address:
+ context.binaryView.file.navigate(context.binaryView.file.view, snippetGlobals['here'])
+ if snippetGlobals['current_address'] != context.address:
+ context.binaryView.file.navigate(context.binaryView.file.view, snippetGlobals['current_address'])
+
+ def makeSnippetFunction(self, code):
+ return lambda context: self.executeSnippet(code, context)
+
+ def registerAllSnippets(self):
+ for action in list(filter(lambda x: x.startswith("Snippet\\"), UIAction.getAllRegisteredActions())):
+ UIAction.registerAction(action, [])
+
+ for snippet in includeWalk(snippetPath, ".py"):
+ (snippetDescription, snippetKey, snippetCode) = loadSnippetFromFile(snippet)
+ if not snippetDescription:
+ actionText = "Snippet\\" + snippet
+ else:
+ actionText = "Snippet\\" + snippetDescription
+ UIAction.registerAction(actionText, snippetKey)
+ UIActionHandler.globalActions().bindAction(actionText, UIAction(self.makeSnippetFunction(snippetCode)))
+
+ def clearSelection(self):
+ self.keySequenceEdit.clear()
+ self.currentHotkey = QKeySequence()
+ self.currentHotkeyLabel.setText("")
+ self.currentFileLabel.setText("")
+ self.snippetDescription.setText("")
+ self.edit.setPlainText("")
+
+ def reject(self):
+ self.settings.setValue("ui/snippeteditor/geometry", self.saveGeometry())
+
+ if self.snippetChanged():
+ question = QMessageBox.question(self, self.tr("Discard"), self.tr("You have unsaved changes, quit anyway?"))
+ if question != QMessageBox.StandardButton.Yes:
+ return
+ self.accept()
+
+ def newFolder(self):
+ (folderName, ok) = QInputDialog.getText(self, self.tr("Folder Name"), self.tr("Folder Name: "))
+ if ok and folderName:
+ index = self.tree.selectionModel().currentIndex()
+ selection = self.files.filePath(index)
+ if QFileInfo(selection).isDir():
+ QDir(selection).mkdir(folderName)
+ else:
+ QDir(snippetPath).mkdir(folderName)
+
+ def selectFile(self, new, old):
+ if (self.resetting):
+ self.resetting = False
+ return
+ newSelection = self.files.filePath(new.indexes()[0])
+ if QFileInfo(newSelection).isDir():
+ self.clearSelection()
+ return
+
+ if old.length() > 0:
+ oldSelection = self.files.filePath(old.indexes()[0])
+ if not QFileInfo(oldSelection).isDir() and self.snippetChanged():
+ question = QMessageBox.question(self, self.tr("Discard"), self.tr("Snippet changed. Discard changes?"))
+ if question != QMessageBox.StandardButton.Yes:
+ self.resetting = True
+ self.tree.selectionModel().select(old, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
+ return False
+
+ self.currentFile = newSelection
+ self.loadSnippet()
+
+ def loadSnippet(self):
+ self.currentFileLabel.setText(QFileInfo(self.currentFile).baseName())
+ log_debug("Loading %s as a snippet." % self.currentFile)
+ (snippetDescription, snippetKey, snippetCode) = loadSnippetFromFile(self.currentFile)
+ self.snippetDescription.setText(snippetDescription) if snippetDescription else self.snippetDescription.setText("")
+ self.keySequenceEdit.setKeySequence(snippetKey[0]) if len(snippetKey) != 0 else self.keySequenceEdit.setKeySequence(QKeySequence(""))
+ self.edit.setPlainText(snippetCode) if snippetCode else self.edit.setPlainText("")
+
+ def newFileDialog(self):
+ (snippetName, ok) = QInputDialog.getText(self, self.tr("Snippet Name"), self.tr("Snippet Name: "))
+ if ok and snippetName:
+ if not snippetName.endswith(".py"):
+ snippetName += ".py"
+ index = self.tree.selectionModel().currentIndex()
+ selection = self.files.filePath(index)
+ if QFileInfo(selection).isDir():
+ open(os.path.join(selection, snippetName), "w").close()
+ else:
+ open(os.path.join(snippetPath, snippetName), "w").close()
+ log_debug("Snippet %s created." % snippetName)
+
+ def deleteSnippet(self):
+ selection = self.tree.selectedIndexes()[::self.columns][0] #treeview returns each selected element in the row
+ snippetName = self.files.fileName(selection)
+ question = QMessageBox.question(self, self.tr("Confirm"), self.tr("Confirm deletion: ") + snippetName)
+ if (question == QMessageBox.StandardButton.Yes):
+ log_debug("Deleting snippet %s." % snippetName)
+ self.clearSelection()
+ self.files.remove(selection)
+
+ def snippetChanged(self):
+ if (self.currentFile == "" or QFileInfo(self.currentFile).isDir()):
+ return False
+ (snippetDescription, snippetKey, snippetCode) = loadSnippetFromFile(self.currentFile)
+ if (not snippetCode):
+ return False
+ if len(snippetKey) == 0 and not self.keySequenceEdit.keySequence().isEmpty():
+ return True
+ if len(snippetKey) != 0 and snippetKey[0] != self.keySequenceEdit.keySequence():
+ return True
+ return self.edit.toPlainText() != snippetCode or \
+ self.snippetDescription.text() != snippetDescription
+
+ def save(self):
+ log_debug("Saving snippet %s" % self.currentFile)
+ outputSnippet = open(self.currentFile, "w")
+ outputSnippet.write("#" + self.snippetDescription.text() + "\n")
+ outputSnippet.write("#" + self.keySequenceEdit.keySequence().toString() + "\n")
+ outputSnippet.write(self.edit.toPlainText())
+ outputSnippet.close()
+ self.registerAllSnippets()
+
+ def clearHotkey(self):
+ self.keySequenceEdit.clear()
+
+def launchPlugin(bv):
+ snippets = Snippets()
+ snippets.exec_()
+
+if __name__ == '__main__':
+ app = QApplication(sys.argv)
+ snippets = Snippets()
+ snippets.show()
+ sys.exit(app.exec_())
+else:
+ PluginCommand.register("Snippet Editor", "Sample UI Plugin for small code snippets to be able to executed on a hotkey.", launchPlugin)
+ #MainThreadActionHandler.register()