diff --git a/src/pytomb/tomblib/parser.py b/src/pytomb/tomblib/parser.py index 5edc7cf..e9eb7f9 100644 --- a/src/pytomb/tomblib/parser.py +++ b/src/pytomb/tomblib/parser.py @@ -3,7 +3,13 @@ Utilities to analyze tomb output ''' import re -_err_regex = re.compile(r'\[!\][^ ]* +(.+)$') +#found: [m] followed by some ID (usually "found") inside square brackets, then +#something else, then a space, then the content +_found_regex = re.compile(r'^\[m\]\[([^]]+)\] +(([^:]+)://(.+))$') +#generic: programname, then some identifiers in square (or round) brackets, +#then maybe something else, then a space, then the context +_generic_regex = re.compile(r'^[a-z-]+ [[(]([^]]+)[\])] +(.+)$') +types = {'E':'error', 'W':'warning', 'D':'debug', '*':'success'} def parse_line(line): '''Analyze a single line. Return None if no standard format is detected, a dict otherwise. @@ -12,9 +18,17 @@ def parse_line(line): 'type' can be 'error', 'progress' ''' - match = _err_regex.search(line) + + match = _found_regex.match(line) if match: - return { 'type': 'error', 'content': match.group(1) } + return { 'type': types.get(match.group(1)) or match.group(1), + 'content': match.group(2), 'scheme': match.group(3), + 'path': match.group(4) } + match = _generic_regex.search(line) + if match: + return { 'type': types.get(match.group(1)) or match.group(1), + 'content': match.group(2) } + return None diff --git a/src/pytomb/tomblib/test_parser.py b/src/pytomb/tomblib/test_parser.py index d601f4d..4ea2334 100644 --- a/src/pytomb/tomblib/test_parser.py +++ b/src/pytomb/tomblib/test_parser.py @@ -1,29 +1,52 @@ from tomblib.parser import * class TestWrong: - def test_wrong_tag(self): - assert parse_line(' [a] foo') is None - def test_no_space(self): - assert parse_line(' [!]foo') is None + def test_short(self): + '''short format is not supported anymore''' + assert parse_line('[!] foo') is None + def test_colors(self): + '''parsing while using colors should fail''' + parse = parse_line('\033[32mundertaker [W] url protocol not recognized: nonscheme') + assert parse is None + def test_no_spaces_in_programname(self): + parse = parse_line('tomb open [W] url protocol not recognized: nonscheme') + assert parse is None -class TestError: +class TestFound: def test_simple(self): - parse = parse_line('[!] foo') + parse = parse_line('[m][found] scheme:///and/path') assert parse is not None - assert parse['type'] == 'error' - assert parse['content'] == 'foo' - def test_preceding(self): - parse = parse_line(' [!] foo') + assert parse['type'] == 'found' + assert parse['content'] == 'scheme:///and/path' + assert 'scheme' in parse + assert parse['scheme'] == 'scheme' + assert 'path' in parse + assert parse['path'] == '/and/path' + +class TestGeneric: + def test_simple(self): + parse = parse_line('undertaker [W] url protocol not recognized: nonscheme') assert parse is not None - assert parse['type'] == 'error' - assert parse['content'] == 'foo' - def test_following(self): - parse = parse_line('[!]shdad foo') + assert parse['type'] == 'warning' + assert parse['content'] == 'url protocol not recognized: nonscheme' + + def test_debug(self): + parse = parse_line('undertaker [D] url protocol not recognized: nonscheme') assert parse is not None - assert parse['type'] == 'error' - assert parse['content'] == 'foo' - def test_mul_words(self): - parse = parse_line('[!] shdad foo') + assert parse['type'] == 'debug' + assert parse['content'] == 'url protocol not recognized: nonscheme' + + def test_success(self): + parse = parse_line('undertaker (*) url protocol not recognized: nonscheme') assert parse is not None - assert parse['type'] == 'error' - assert parse['content'] == 'shdad foo' + assert parse['type'] == 'success' + assert parse['content'] == 'url protocol not recognized: nonscheme' + + def test_dash(self): + parse = parse_line('tomb-open [W] url protocol not recognized: nonscheme') + assert parse is not None + assert parse['type'] == 'warning' + assert parse['content'] == 'url protocol not recognized: nonscheme' + + + diff --git a/src/pytomb/tomblib/undertaker.py b/src/pytomb/tomblib/undertaker.py new file mode 100644 index 0000000..d73e740 --- /dev/null +++ b/src/pytomb/tomblib/undertaker.py @@ -0,0 +1,79 @@ +import subprocess +from tempfile import NamedTemporaryFile + +import parser + +class Undertaker(object): + ''' + This is similar to Tomb class, and provides a wrapper on undertaker. + + TODO: + * methods for automagical scan + * output parsing, giving meaningful output + + Due to the non-interactive nature of undertaker, it's simpler than Tomb + ''' + undertakerexec = 'undertaker' + @classmethod + def check(cls, paths): + '''Will check if there are keys available there, as in --path + + paths can be a string (one address), or a list of + ''' + #TODO: more solid check: something like + if type(paths) is not str: + out = [] + for p in paths: + try: + res = cls.check(p) + except: + continue + else: + if res: + out.extend(res) + return out + + buf = NamedTemporaryFile() + try: + subprocess.check_call([cls.undertakerexec, paths, '--batch', + '--path'], stdout=buf) + except subprocess.CalledProcessError as exc: + return False + + out = [] + buf.seek(0) + for line in buf: + ret = parser.parse_line(line) + if ret and ret['type'] == 'found': + out.append(ret['content']) + return out + + + @classmethod + def get(cls, paths): + ''' + Similar to check, but truly get the key content. + If paths is iterable, stop at the first successful path + ''' + if type(paths) is not str: + for p in paths: + try: + res = cls.get(p) + except: + continue + else: + if res: + return res + buf = NamedTemporaryFile() + try: + subprocess.check_call([cls.undertakerexec, paths, '--batch'], + stdout=buf) + except subprocess.CalledProcessError: + return False + buf.seek(0) + return buf.read() + + +if __name__ == '__main__': + Undertaker.undertakerexec = '/home/davide/coding/projects/tomb/src/undertaker' + print Undertaker.get('near:///home/davide/Desktop/testing.tomb') diff --git a/src/qt/.gitignore b/src/qt/.gitignore index dd75296..cf9dc94 100644 --- a/src/qt/.gitignore +++ b/src/qt/.gitignore @@ -2,3 +2,5 @@ *.pyc TombQt.egg-info build +dist +disk diff --git a/src/qt/tombqt/open.py b/src/qt/tombqt/open.py new file mode 100644 index 0000000..e2171bd --- /dev/null +++ b/src/qt/tombqt/open.py @@ -0,0 +1,109 @@ +import sys + +from PyQt4 import QtCore, QtGui + +from ui_open_tombfile import Ui_tombfile +from ui_open_keymethod import Ui_keymethod +from ui_open_success import Ui_success + +from tomblib.tomb import Tomb +from tomblib.undertaker import Undertaker + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class TombfilePage(QtGui.QWizardPage): + def __init__(self, *args, **kwargs): + QtGui.QWizardPage.__init__(self, *args) + self.ui = Ui_tombfile() + self.ui.setupUi(self) + if 'tombfile' in kwargs and kwargs['tombfile'] is not None: + self.ui.tomb_line.setText(kwargs['tombfile']) + self.ui.tomb_browse.clicked.connect(self.on_tomb_location_clicked) + def on_tomb_location_clicked(self, *args, **kwargs): + filename = QtGui.QFileDialog.getOpenFileName(self, 'Select Tomb', + filter="Tomb (*.tomb)") + self.ui.tomb_line.setText(filename) + +class MethodPage(QtGui.QWizardPage): + def __init__(self, *args, **kwargs): + QtGui.QWizardPage.__init__(self, *args, **kwargs) + self.ui = Ui_keymethod() + self.ui.setupUi(self) + self.group = group = QtGui.QButtonGroup() + for radio in self.children(): + if type(radio) == QtGui.QRadioButton: + group.addButton(radio) + + def initializePage(self): + self.found = Undertaker.check( str('near://' + self.wizard().get_tombfile()) ) or [] + box = self.ui.radio_layout + + for key in self.found: + radio = QtGui.QRadioButton('Automatically found: ' + key, parent=self) + radio.setChecked(True) + radio.setProperty('path', key) + box.insertWidget(0, radio) + self.group.addButton(radio) + + + def nextId(self): + '''Virtual method reimplemented to decide next page''' + if self.ui.fs.isChecked(): + keyfile = QtGui.QFileDialog.getOpenFileName(self.wizard(), 'Key file', + filter="Tomb keys (*.tomb.key);;Buried keys (*.jpeg)") + if keyfile: + #TODO: check if this really is a success :) + if Tomb.open(self.wizard().get_tombfile(), keyfile): #bugs when wrong password + return TombOpenWizard.SUCCESS_PAGE + #else: #TODO: should alert the user that we failed + return TombOpenWizard.METHOD_PAGE + if self.ui.usb.isChecked(): + return TombOpenWizard.USB_PAGE + print self.group.checkedButton().property('path').toPyObject() + return TombOpenWizard.SUCCESS_PAGE + +class SuccessPage(QtGui.QWizardPage): + def __init__(self, *args, **kwargs): + QtGui.QWizardPage.__init__(self, *args, **kwargs) + self.ui = Ui_success() + self.ui.setupUi(self) + +class TombOpenWizard(QtGui.QWizard): + TOMBFILE_PAGE=1 + METHOD_PAGE=2 + SUCCESS_PAGE=99 + USB_PAGE=20 + def __init__(self, *args, **kwargs): + QtGui.QWizard.__init__(self, *args) + self.setPage(TombOpenWizard.TOMBFILE_PAGE, + TombfilePage(self, tombfile = kwargs['tombfile'] + if 'tombfile' in kwargs else None)) + self.setPage(TombOpenWizard.METHOD_PAGE, MethodPage(self)) + self.setPage(TombOpenWizard.SUCCESS_PAGE, SuccessPage(self)) + if 'tombfile' in kwargs and kwargs['tombfile'] is not None: + self.setStartId(TombOpenWizard.METHOD_PAGE) + + def get_tombfile(self): + page = self.page(TombOpenWizard.TOMBFILE_PAGE) + return page.ui.tomb_line.text() + + + +def run_open_wizard(): + app = QtGui.QApplication(sys.argv) + window = TombOpenWizard(tombfile=sys.argv[1] if len(sys.argv) > 1 else None) + window.show() + sys.exit(app.exec_()) + +if __name__ == '__main__': + Undertaker.undertakerexec = '/home/davide/coding/projects/tomb/src/undertaker' + run_open_wizard() + + + + + + diff --git a/src/qt/tombqt/open_keymethod.ui b/src/qt/tombqt/open_keymethod.ui new file mode 100644 index 0000000..2299936 --- /dev/null +++ b/src/qt/tombqt/open_keymethod.ui @@ -0,0 +1,84 @@ + + + keymethod + + + + 0 + 0 + 480 + 640 + + + + WizardPage + + + Choose key + + + + + + Qt::Vertical + + + + 20 + 265 + + + + + + + + + + Filesystem + + + true + + + + + + + false + + + USB drive + + + + + + + false + + + Retrieve via bluetooth (advanced) + + + + + + + + + Qt::Vertical + + + + 20 + 265 + + + + + + + + + diff --git a/src/qt/tombqt/open_success.ui b/src/qt/tombqt/open_success.ui new file mode 100644 index 0000000..200e4fd --- /dev/null +++ b/src/qt/tombqt/open_success.ui @@ -0,0 +1,34 @@ + + + success + + + + 0 + 0 + 480 + 640 + + + + WizardPage + + + Tomb opened + + + success + + + + + + You successfully opened the tomb + + + + + + + + diff --git a/src/qt/tombqt/open_tombfile.ui b/src/qt/tombqt/open_tombfile.ui new file mode 100644 index 0000000..3b73ab2 --- /dev/null +++ b/src/qt/tombqt/open_tombfile.ui @@ -0,0 +1,79 @@ + + + tombfile + + + + 0 + 0 + 480 + 640 + + + + WizardPage + + + Choose tomb + + + + + + Qt::Vertical + + + + 20 + 276 + + + + + + + + + + Choose a tomb file on your filesystem + + + + + + + + + /path/to/your.tomb + + + + + + + Browse + + + + + + + + + + + Qt::Vertical + + + + 20 + 276 + + + + + + + + + diff --git a/src/qt/tombqt/ui_open_keymethod.py b/src/qt/tombqt/ui_open_keymethod.py new file mode 100644 index 0000000..21b9951 --- /dev/null +++ b/src/qt/tombqt/ui_open_keymethod.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'tombqt/open_keymethod.ui' +# +# Created: Sat Jan 28 03:36:11 2012 +# by: PyQt4 UI code generator 4.9 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_keymethod(object): + def setupUi(self, keymethod): + keymethod.setObjectName(_fromUtf8("keymethod")) + keymethod.resize(480, 640) + self.verticalLayout = QtGui.QVBoxLayout(keymethod) + self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) + spacerItem = QtGui.QSpacerItem(20, 265, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem) + self.radio_layout = QtGui.QVBoxLayout() + self.radio_layout.setObjectName(_fromUtf8("radio_layout")) + self.fs = QtGui.QRadioButton(keymethod) + self.fs.setObjectName(_fromUtf8("fs")) + self.radio_layout.addWidget(self.fs) + self.usb = QtGui.QRadioButton(keymethod) + self.usb.setEnabled(False) + self.usb.setObjectName(_fromUtf8("usb")) + self.radio_layout.addWidget(self.usb) + self.bluetooth = QtGui.QRadioButton(keymethod) + self.bluetooth.setEnabled(False) + self.bluetooth.setObjectName(_fromUtf8("bluetooth")) + self.radio_layout.addWidget(self.bluetooth) + self.verticalLayout.addLayout(self.radio_layout) + spacerItem1 = QtGui.QSpacerItem(20, 265, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem1) + + self.retranslateUi(keymethod) + QtCore.QMetaObject.connectSlotsByName(keymethod) + + def retranslateUi(self, keymethod): + keymethod.setWindowTitle(QtGui.QApplication.translate("keymethod", "WizardPage", None, QtGui.QApplication.UnicodeUTF8)) + keymethod.setTitle(QtGui.QApplication.translate("keymethod", "Choose key", None, QtGui.QApplication.UnicodeUTF8)) + self.fs.setText(QtGui.QApplication.translate("keymethod", "Filesystem", None, QtGui.QApplication.UnicodeUTF8)) + self.usb.setText(QtGui.QApplication.translate("keymethod", "USB drive", None, QtGui.QApplication.UnicodeUTF8)) + self.bluetooth.setText(QtGui.QApplication.translate("keymethod", "Retrieve via bluetooth (advanced)", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/src/qt/tombqt/ui_open_success.py b/src/qt/tombqt/ui_open_success.py new file mode 100644 index 0000000..700081f --- /dev/null +++ b/src/qt/tombqt/ui_open_success.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'tombqt/open_success.ui' +# +# Created: Mon Jan 23 23:06:38 2012 +# by: PyQt4 UI code generator 4.9 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_success(object): + def setupUi(self, success): + success.setObjectName(_fromUtf8("success")) + success.resize(480, 640) + self.verticalLayout = QtGui.QVBoxLayout(success) + self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) + self.label = QtGui.QLabel(success) + self.label.setObjectName(_fromUtf8("label")) + self.verticalLayout.addWidget(self.label) + + self.retranslateUi(success) + QtCore.QMetaObject.connectSlotsByName(success) + + def retranslateUi(self, success): + success.setWindowTitle(QtGui.QApplication.translate("success", "WizardPage", None, QtGui.QApplication.UnicodeUTF8)) + success.setTitle(QtGui.QApplication.translate("success", "Tomb opened", None, QtGui.QApplication.UnicodeUTF8)) + success.setSubTitle(QtGui.QApplication.translate("success", "success", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("success", "You successfully opened the tomb", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/src/qt/tombqt/ui_open_tombfile.py b/src/qt/tombqt/ui_open_tombfile.py new file mode 100644 index 0000000..d9f1eb6 --- /dev/null +++ b/src/qt/tombqt/ui_open_tombfile.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'tombqt/open_tombfile.ui' +# +# Created: Tue Jan 24 00:49:10 2012 +# by: PyQt4 UI code generator 4.9 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_tombfile(object): + def setupUi(self, tombfile): + tombfile.setObjectName(_fromUtf8("tombfile")) + tombfile.resize(480, 640) + self.verticalLayout_2 = QtGui.QVBoxLayout(tombfile) + self.verticalLayout_2.setObjectName(_fromUtf8("verticalLayout_2")) + spacerItem = QtGui.QSpacerItem(20, 276, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) + self.verticalLayout_2.addItem(spacerItem) + self.verticalLayout = QtGui.QVBoxLayout() + self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) + self.label = QtGui.QLabel(tombfile) + self.label.setObjectName(_fromUtf8("label")) + self.verticalLayout.addWidget(self.label) + self.horizontalLayout = QtGui.QHBoxLayout() + self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) + self.tomb_line = QtGui.QLineEdit(tombfile) + self.tomb_line.setObjectName(_fromUtf8("tomb_line")) + self.horizontalLayout.addWidget(self.tomb_line) + self.tomb_browse = QtGui.QPushButton(tombfile) + self.tomb_browse.setObjectName(_fromUtf8("tomb_browse")) + self.horizontalLayout.addWidget(self.tomb_browse) + self.verticalLayout.addLayout(self.horizontalLayout) + self.verticalLayout_2.addLayout(self.verticalLayout) + spacerItem1 = QtGui.QSpacerItem(20, 276, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) + self.verticalLayout_2.addItem(spacerItem1) + + self.retranslateUi(tombfile) + QtCore.QMetaObject.connectSlotsByName(tombfile) + + def retranslateUi(self, tombfile): + tombfile.setWindowTitle(QtGui.QApplication.translate("tombfile", "WizardPage", None, QtGui.QApplication.UnicodeUTF8)) + tombfile.setTitle(QtGui.QApplication.translate("tombfile", "Choose tomb", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("tombfile", "Choose a tomb file on your filesystem", None, QtGui.QApplication.UnicodeUTF8)) + self.tomb_line.setPlaceholderText(QtGui.QApplication.translate("tombfile", "/path/to/your.tomb", None, QtGui.QApplication.UnicodeUTF8)) + self.tomb_browse.setText(QtGui.QApplication.translate("tombfile", "Browse", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/src/undertaker b/src/undertaker index 084083c..ec64e3c 100755 --- a/src/undertaker +++ b/src/undertaker @@ -37,7 +37,7 @@ fi key_found() { # $1 is "url" - if option_is_set --machine-parseable; then + if option_is_set --batch; then print -n '[m]' fi print "$fg[white][found] $1" @@ -46,7 +46,7 @@ key_found() { function undertaker_scheme() { - zparseopts -D -print-path=print_path + zparseopts -D -path=print_path local scheme scheme=$1 @@ -60,7 +60,7 @@ function undertaker_scheme() { act "access to bluetooth protocol requested" which obexftp &> /dev/null if [[ $? != 0 ]]; then - error "obexftp not found, needed for bluetooth: operation aborted." + _warning "obexftp not found, needed for bluetooth: operation aborted." return 64 fi keytmp=`safe_dir undertaker` @@ -95,10 +95,10 @@ function undertaker_scheme() { file) if ! [[ -f $keypath ]]; then - error "Invalid path $keypath" + _warning "Invalid path $keypath" return 1 fi - if [[ -n $print_path ]]; then + if option_is_set --path; then key_found $scheme://$keypath; else < $keypath @@ -122,7 +122,7 @@ function undertaker_scheme() { #It implements automounting using udisks; udisks is a (recently) #new technology, so we can't rely on it being present if ! which udisks &> /dev/null; then - error 'udisks not found' + _warning 'udisks not found' exit 64 fi while true; do @@ -149,7 +149,7 @@ function undertaker_scheme() { *) if ! which undertaker-$scheme &> /dev/null; then - error "url protocol not recognized: $scheme" + _warning "url protocol not recognized: $scheme" return 64 fi undertaker-$scheme ${print_path[@]} ${scheme}://$keypath @@ -162,13 +162,14 @@ function main() { typeset -A opts zparseopts -M -E -D -Aopts -poll -path -batch if ! [ $1 ] ; then - error "an argument is missing, the undertaker is confused" - act "usage: undertaker [options] url://host:path/to/tomb.key" + echo "[W] an argument is missing, the undertaker is confused" + echo "usage: undertaker [options] url://host:path/to/tomb.key" exit 1; fi local -a tomb_opts if [[ -n ${(k)opts[--batch]} ]]; then - tomb_opts+='--batch' + tomb_opts+='--no-color' + tomb_opts+='--quiet' fi local -a under_opts if [[ -n ${(k)opts[--path]} ]]; then @@ -178,13 +179,14 @@ function main() { for a in ${(k)opts}; do backupopts[$a]=${opts[$a]} done - source tomb ${tomb_opts[@]} source + source tomb ${tomb_opts[@]} source + TOMBEXEC=undertaker for a in ${(k)backupopts}; do opts[$a]=${backupopts[$a]} done check_bin - notice "Undertaker will look for $1" + _success "Undertaker will look for $1" ARG1=${1} scheme=${ARG1%://*}