From 9f8dab3209e42a7b69a8a7a1ae100d707a4fc558 Mon Sep 17 00:00:00 2001 From: jvoisin Date: Tue, 16 Aug 2011 18:43:58 +0200 Subject: Change test according to previous commit --- cli.py | 152 --------------- gui.py | 571 -------------------------------------------------------- test/clitest.py | 12 +- 3 files changed, 6 insertions(+), 729 deletions(-) delete mode 100755 cli.py delete mode 100644 gui.py diff --git a/cli.py b/cli.py deleted file mode 100755 index 804c4cf..0000000 --- a/cli.py +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/python -''' - Metadata anonymisation toolkit - CLI edition -''' - -import sys -import xml.sax -import optparse - -import hachoir_core - -from mat import mat - -__version__ = '0.1' - - -def parse(): - ''' - Get, and parse options passed to the program - ''' - parser = optparse.OptionParser(usage='%prog [options] files') - parser.add_option('--add2archive', '-a', action='store_true', - default=False, help='Add to outputed archive non-supported filetypes') - parser.add_option('--backup', '-b', action='store_true', default=False, - help='Keep a backup copy') - parser.add_option('--check', '-c', action='store_true', default=False, - help='Check if a file is free of harmfull metadatas') - parser.add_option('--display', '-d', action='store_true', default=False, - help='List all the meta of a file without removing them') - parser.add_option('--force', '-f', action='store_true', default=False, - help='Don\'t check if files are clean before cleaning') - parser.add_option('--list', '-l', action='store_true', default=False, - help='List all supported fileformat') - parser.add_option('--ugly', '-u', action='store_true', default=False, - help='Remove harmful meta, but loss can occure') - parser.add_option('--version', '-v', action='callback', - callback=display_version, help='Display version and exit') - - values, arguments = parser.parse_args() - if not arguments and values.list is False: - parser.print_help() - sys.exit(0) - return values, arguments - - -def display_version(*_): - ''' - Display the program's version, and exit - ''' - print('Metadata Anonymisation Toolkit version %s') % mat.__version__ - print('CLI version %s') % __version__ - print('Hachoir version %s') % hachoir_core.__version__ - sys.exit(0) - - -def list_meta(class_file, filename, force): - ''' - Print all the meta of 'filename' on stdout - ''' - print('[+] File %s :' % filename) - if force is False and class_file.is_clean(): - print('No harmful meta found') - else: - meta = class_file.get_meta() - if meta is None: - print('No harmful meta found') - else: - for key, value in class_file.get_meta().iteritems(): - print(key + ' : ' + str(value)) - - -def is_clean(class_file, filename, force): - ''' - Say if 'filename' is clean or not - ''' - if class_file.is_clean(): - print('[+] %s is clean' % filename) - else: - print('[+] %s is not clean' % filename) - - -def clean_meta(class_file, filename, force): - ''' - Clean the file 'filename' - ''' - print('[+] Cleaning %s' % filename) - if force is False and class_file.is_clean(): - print('%s is already clean' % filename) - else: - class_file.remove_all() - print('%s cleaned !' % filename) - - -def clean_meta_ugly(class_file, filename, force): - ''' - Clean the file 'filename', ugly way - ''' - print('[+] Cleaning %s' % filename) - if force is False and class_file.is_clean(): - print('%s is already clean' % filename) - else: - class_file.remove_all_ugly() - print('%s cleaned' % filename) - - -def list_supported(): - ''' - Print all supported fileformat, and exit - ''' - handler = mat.XMLParser() - parser = xml.sax.make_parser() - parser.setContentHandler(handler) - with open('FORMATS', 'r') as xmlfile: - parser.parse(xmlfile) - - for item in handler.list: - print('%s (%s)' % (item['name'], item['extension'])) - print('\tsupport : ' + item['support']) - print('\tmetadata : ' + item['metadata']) - print('\tmethod : ' + item['method']) - if item['support'] == 'partial': - print('\tremaining : ' + item['remaining']) - print('\n') - sys.exit(0) - - -def main(): - ''' - main function : get args, and launch the appropriate function - ''' - args, filenames = parse() - - #func receive the function correponding to the options given as parameters - if args.display is True: # only print metadatas - func = list_meta - elif args.check is True: # only check if the file is clean - func = is_clean - elif args.ugly is True: # destructive anonymisation method - func = clean_meta_ugly - elif args.list is True: # print the list of all supported format - list_supported() - else: # clean the file - func = clean_meta - - for filename in filenames: - class_file = mat.create_class_file(filename, args.backup, - args.add2archive) - if class_file is not None: - func(class_file, filename, args.force) - -if __name__ == '__main__': - main() diff --git a/gui.py b/gui.py deleted file mode 100644 index 07b8d6c..0000000 --- a/gui.py +++ /dev/null @@ -1,571 +0,0 @@ -#!/usr/bin/env python -#charset utf-8 - -''' - Metadata anonymisation toolkit - GUI edition -''' - -import gtk -import gobject - -import gettext -import locale -import logging -import os -import xml.sax - -from mat import mat - -__version__ = '0.1' -__author__ = 'jvoisin' - - - -logging.basicConfig(level=mat.LOGGING_LEVEL) - - -class CFile(object): - ''' - Contain the "parser" class of the file "filename" - This class exist just to be "around" my parser.Generic_parser class, - since the gtk.ListStore does not accept it. - ''' - def __init__(self, filename, backup, add2archive): - try: - self.file = mat.create_class_file(filename, backup, add2archive) - except: - self.file = None - - -class GUI: - ''' - Main GUI class - ''' - def __init__(self): - # Preferences - self.force = False - self.backup = True - self.add2archive = True - - # Main window - self.window = gtk.Window() - self.window.set_title('Metadata Anonymisation Toolkit %s' % - __version__) - self.window.connect('destroy', gtk.main_quit) - self.window.set_default_size(800, 600) - - vbox = gtk.VBox() - self.window.add(vbox) - - menubar = self.create_menu() - toolbar = self.create_toolbar() - content = gtk.ScrolledWindow() - vbox.pack_start(menubar, False, True, 0) - vbox.pack_start(toolbar, False, True, 0) - vbox.pack_start(content, True, True, 0) - - # parser.class - name - type - cleaned - self.liststore = gtk.ListStore(object, str, str, str) - - treeview = gtk.TreeView(model=self.liststore) - treeview.set_search_column(1) # name column is searchable - treeview.set_rules_hint(True) # alternate colors for rows - treeview.set_rubber_banding(True) # mouse selection - self.add_columns(treeview) - self.selection = treeview.get_selection() - self.selection.set_mode(gtk.SELECTION_MULTIPLE) - - content.add(treeview) - - self.statusbar = gtk.Statusbar() - self.statusbar.push(1, _('Ready')) - vbox.pack_start(self.statusbar, False, False, 0) - - self.window.show_all() - - def create_toolbar(self): - ''' - Returns a vbox object, which contains a toolbar with buttons - ''' - toolbar = gtk.Toolbar() - - toolbutton = gtk.ToolButton(gtk.STOCK_ADD) - toolbutton.set_label(_('Add')) - toolbutton.connect('clicked', self.add_files) - toolbutton.set_tooltip_text(_('Add files')) - toolbar.add(toolbutton) - - toolbutton = gtk.ToolButton(gtk.STOCK_PRINT_REPORT) - toolbutton.set_label(_('Clean')) - toolbutton.connect('clicked', self.process_files, self.mat_clean) - toolbutton.set_tooltip_text(_('Clean selected files without data loss')) - toolbar.add(toolbutton) - - toolbutton = gtk.ToolButton(gtk.STOCK_PRINT_WARNING) - toolbutton.set_label(_('Brute Clean')) - toolbutton.connect('clicked', self.mat_clean_dirty) - toolbutton.set_tooltip_text(_('Clean selected files with possible \ -data loss')) - toolbar.add(toolbutton) - - toolbutton = gtk.ToolButton(gtk.STOCK_FIND) - toolbutton.set_label(_('Check')) - toolbutton.connect('clicked', self.process_files, self.mat_check) - toolbutton.set_tooltip_text(_('Check selected files for harmful meta')) - toolbar.add(toolbutton) - - toolbutton = gtk.ToolButton(stock_id=gtk.STOCK_QUIT) - toolbutton.set_label(_('Quit')) - toolbutton.connect('clicked', gtk.main_quit) - toolbar.add(toolbutton) - - vbox = gtk.VBox(spacing=3) - vbox.pack_start(toolbar, False, False, 0) - return vbox - - def add_columns(self, treeview): - ''' - Create the columns, and add them to the treeview - ''' - colname = [_('Filename'), _('Mimetype'), _('State')] - - for i, j in enumerate(colname): - filename_column = gtk.CellRendererText() - column = gtk.TreeViewColumn(j, filename_column, text=i + 1) - column.set_sort_column_id(i + 1) - if i is 0: # place tooltip on this column - tips = TreeViewTooltips(column) - tips.add_view(treeview) - treeview.append_column(column) - - def create_menu_item(self, name, func, menu, pix): - ''' - Create a MenuItem() like Preferences, Quit, Add, Clean, ... - ''' - item = gtk.ImageMenuItem() - picture = gtk.Image() - picture.set_from_stock(pix, gtk.ICON_SIZE_MENU) - item.set_image(picture) - item.set_label(name) - item.connect('activate', func) - menu.append(item) - - def create_sub_menu(self, name, menubar): - ''' - Create a submenu like File, Edit, Clean, ... - ''' - submenu = gtk.Menu() - menuitem = gtk.MenuItem() - menuitem.set_submenu(submenu) - menuitem.set_label(name) - menubar.append(menuitem) - return submenu - - def create_menu(self): - ''' - Return a MenuBar - ''' - menubar = gtk.MenuBar() - - file_menu = self.create_sub_menu(_('Files'), menubar) - self.create_menu_item(_('Add files'), self.add_files, file_menu, - gtk.STOCK_ADD) - self.create_menu_item(_('Quit'), gtk.main_quit, file_menu, - gtk.STOCK_QUIT) - - edit_menu = self.create_sub_menu(_('Edit'), menubar) - self.create_menu_item(_('Clear the filelist'), - lambda x: self.liststore.clear(), edit_menu, gtk.STOCK_REMOVE) - self.create_menu_item(_('Preferences'), self.preferences, edit_menu, - gtk.STOCK_PREFERENCES) - - process_menu = self.create_sub_menu(_('Process'), menubar) - item = gtk.ImageMenuItem() - picture = gtk.Image() - picture.set_from_stock(gtk.STOCK_PRINT_REPORT, gtk.ICON_SIZE_MENU) - item.set_image(picture) - item.set_label(_('Clean')) - item.connect('activate', self.process_files, self.mat_clean) - process_menu.append(item) - - item = gtk.ImageMenuItem() - picture = gtk.Image() - picture.set_from_stock(gtk.STOCK_PRINT_WARNING, gtk.ICON_SIZE_MENU) - item.set_image(picture) - item.set_label(_('Clean (lossy way)')) - item.connect('activate', self.process_files, self.mat_clean_dirty) - process_menu.append(item) - - item = gtk.ImageMenuItem() - picture = gtk.Image() - picture.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU) - item.set_image(picture) - item.set_label(_('Check')) - item.connect('activate', self.process_files, self.mat_check) - process_menu.append(item) - - - help_menu = self.create_sub_menu(_('Help'), menubar) - self.create_menu_item(_('Supported formats'), self.supported, help_menu, - gtk.STOCK_INFO) - self.create_menu_item(_('About'), self.about, help_menu, gtk.STOCK_ABOUT) - - return menubar - - def add_files(self, button): - ''' - Add the files chosed by the filechoser ("Add" button) - ''' - chooser = gtk.FileChooserDialog(title=_('Choose files'), - parent=self.window, action=gtk.FILE_CHOOSER_ACTION_OPEN, - buttons=(gtk.STOCK_OK, 0, gtk.STOCK_CANCEL, 1)) - chooser.set_default_response(0) - chooser.set_select_multiple(True) - - all_filter = gtk.FileFilter() # filter that shows all files - all_filter.set_name(_('All files')) - all_filter.add_pattern('*') - chooser.add_filter(all_filter) - - supported_filter = gtk.FileFilter() - # filter that shows only supported formats - [supported_filter.add_mime_type(i) for i in mat.STRIPPERS.keys()] - supported_filter.set_name(_('Supported files')) - chooser.add_filter(supported_filter) - - response = chooser.run() - - if response is 0: # gtk.STOCK_OK - filenames = chooser.get_filenames() - task = self.populate(filenames) - gobject.idle_add(task.next) # asynchrone processing - chooser.destroy() - - def populate(self, filenames): - ''' - Append selected files by add_file to the self.liststore - ''' - for filename in filenames: # filenames : all selected files/folders - if os.path.isdir(filename): # if "filename" is a directory - for root, dirs, files in os.walk(filename): - for item in files: - path_to_file = os.path.join(root, item) - self.add_file_to_treeview(path_to_file) - else: # filename is a regular file - self.add_file_to_treeview(filename) - yield True - yield False - - def add_file_to_treeview(self, filename): - ''' - Add a file to the list if his format is supported - ''' - cf = CFile(filename, self.backup, self.add2archive) - if cf.file is not None: - self.liststore.append([cf, cf.file.basename, - cf.file.mime, _('unknow')]) - - - def about(self, button): - ''' - About popup - ''' - w = gtk.AboutDialog() - w.set_version(__version__) - w.set_copyright('GNU Public License v2') - w.set_comments(_('This software was coded during the GSoC 2011')) - w.set_website('https://gitweb.torproject.org/user/jvoisin/mat.git') - w.set_website_label(_('Website')) - w.set_authors(['Julien (jvoisin) Voisin', ]) - w.set_program_name('Metadata Anonymistion Toolkit') - click = w.run() - if click: - w.destroy() - - def supported(self, button): - ''' - List the supported formats - ''' - dialog = gtk.Dialog(_('Supported formats'), self.window, 0, - (gtk.STOCK_CLOSE, 0)) - vbox = gtk.VBox(spacing=5) - dialog.get_content_area().pack_start(vbox, True, True, 0) - - label = gtk.Label() - label.set_markup('Supported fileformats') - vbox.pack_start(label, True, True, 0) - - #parsing xml - handler = mat.XMLParser() - parser = xml.sax.make_parser() - parser.setContentHandler(handler) - with open('FORMATS', 'r') as xmlfile: - parser.parse(xmlfile) - - for item in handler.list: # list of dict : one dict per format - #create one expander per format - title = '%s (%s)' % (item['name'], item['extension']) - support = ('\t%s : %s' % ('support', item['support'])) - metadata = '\n\tmetadata : ' + item['metadata'] - method = '\n\tmethod : ' + item['method'] - content = support + metadata + method - if item['support'] == 'partial': - content += '\n\tremaining : ' + item['remaining'] - - expander = gtk.Expander(title) - vbox.pack_start(expander, False, False, 0) - label = gtk.Label() - label.set_markup(content) - expander.add(label) - - dialog.show_all() - click = dialog.run() - if click is 0: # Close - dialog.destroy() - - def preferences(self, button): - ''' - Preferences popup - ''' - dialog = gtk.Dialog(_('Preferences'), self.window, 0, (gtk.STOCK_OK, 0)) - hbox = gtk.HBox() - dialog.get_content_area().pack_start(hbox, False, False, 0) - - icon = gtk.Image() - icon.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_DIALOG) - hbox.pack_start(icon, False, False, 0) - - table = gtk.Table(3, 2, False) # nb rows, nb lines - hbox.pack_start(table, True, True, 0) - - force = gtk.CheckButton(_('Force Clean'), False) - force.set_active(self.force) - force.connect('toggled', self.invert, 'force') - force.set_tooltip_text(_('Do not check if already clean before \ -cleaning')) - table.attach(force, 0, 1, 0, 1) - - backup = gtk.CheckButton(_('Backup'), False) - backup.set_active(self.backup) - backup.connect('toggled', self.invert, 'backup') - backup.set_tooltip_text(_('Keep a backup copy')) - table.attach(backup, 0, 1, 1, 2) - - add2archive = gtk.CheckButton(_('Add unsupported file to archives'), - False) - add2archive.set_active(self.add2archive) - add2archive.connect('toggled', self.invert, 'add2archive') - add2archive.set_tooltip_text(_('Add non-supported (and so \ -non-anonymised) file to outputed archive')) - table.attach(add2archive, 0, 1, 2, 3) - - hbox.show_all() - response = dialog.run() - if response is 0: # gtk.STOCK_OK - dialog.destroy() - - def invert(self, button, name): - ''' - Invert a preference state - ''' - if name == 'force': - self.force = not self.force - elif name == 'backup': - self.backup = not self.backup - elif name == 'add2archive': - self.add2archive = not self.add2archive - - def process_files(self, button, func): - ''' - Launch the function "function" in a asynchrone way - ''' - iterator = self.selection.get_selected_rows()[1] - if not iterator: # if nothing is selected : select everything - iterator = xrange(len(self.liststore)) - task = func(iterator) # launch func() in an asynchrone way - gobject.idle_add(task.next) - - def mat_check(self, iterator): - ''' - Check if selected elements are clean - ''' - for line in iterator: # for each file in selection - if self.liststore[line][0].file.is_clean(): - string = _('clean') - else: - string = _('dirty') - logging.info('%s is %s' % (self.liststore[line][1], string)) - self.liststore[line][3] = string - yield True - yield False - - def mat_clean(self, iterator): - ''' - Clean selected elements - ''' - for line in iterator: # for each file in selection - logging.info('Cleaning %s' % self.liststore[line][1]) - if self.liststore[line][3] is not _('clean'): - if self.force or not self.liststore[line][0].file.is_clean(): - self.liststore[line][0].file.remove_all() - self.liststore[line][3] = _('clean') - yield True - yield False - - def mat_clean_dirty(self, iterator): - ''' - Clean selected elements (ugly way) - ''' - for line in iterator: # for each file in selection - logging.info('Cleaning (lossy way) %s' % self.liststore[line][1]) - if self.liststore[line][3] is not _('clean'): - if self.force or not self.liststore[line][0].file.is_clean(): - self.liststore[line][0].file.remove_all_ugly() - self.liststore[line][3] = _('clean') - yield True - yield False - - -class TreeViewTooltips(object): - ''' - A dirty hack losely based on treeviewtooltip from Daniel J. Popowich - (dpopowich AT astro dot umass dot edu), to display differents tooltips - for each cell of the first row of the GUI. - ''' - # Copyright (c) 2006, Daniel J. Popowich - # - # Permission is hereby granted, free of charge, to any person - # obtaining a copy of this software and associated documentation files - # (the "Software"), to deal in the Software without restriction, - # including without limitation the rights to use, copy, modify, merge, - # publish, distribute, sublicense, and/or sell copies of the Software, - # and to permit persons to whom the Software is furnished to do so, - # subject to the following conditions: - # - # The above copyright notice and this permission notice shall be - # included in all copies or substantial portions of the Software. - def __init__(self, namecol): - ''' - Initialize the tooltip. - window: the popup window that holds the tooltip text, an - instance of gtk.Window. - label: a gtk.Label that is packed into the window. The - tooltip text is set in the label with the - set_label() method, so the text can be plain or - markup text. - ''' - # create the window - self.window = window = gtk.Window(gtk.WINDOW_POPUP) - window.set_name('gtk-tooltips') - window.set_resizable(False) - window.set_border_width(4) - window.set_app_paintable(True) - window.connect("expose-event", self.__on_expose_event) - - # create the label - self.label = label = gtk.Label() - label.set_line_wrap(True) - label.set_alignment(0.5, 0.5) - label.show() - window.add(label) - - self.namecol = namecol - self.__save = None # saves the current cell - self.__next = None # the timer id for the next tooltip to be shown - self.__shown = False # flag on whether the tooltip window is shown - - def __show(self, tooltip, x, y): - ''' - show the tooltip - ''' - self.label.set_label(tooltip) # set label - self.window.move(x, y) # move the window - self.window.show() # show it - self.__shown = True - - def __hide(self): - ''' - hide the tooltip - ''' - self.__queue_next() - self.window.hide() - self.__shown = False - - def __motion_handler(self, view, event): - ''' - as the pointer moves across the view, show a tooltip - ''' - path_at_pos = view.get_path_at_pos(int(event.x), int(event.y)) - if path_at_pos: - path, col, x, y = path_at_pos - tooltip = self.get_tooltip(view, col, path) - if tooltip: - tooltip = str(tooltip).strip() - self.__queue_next((path, col), tooltip, int(event.x_root), - int(event.y_root)) - return - self.__hide() - - def __queue_next(self, *args): - ''' - queue next request to show a tooltip - if args is non-empty it means a request was made to show a - tooltip. if empty, no request is being made, but any - pending requests should be cancelled anyway - ''' - cell = None - - if args: # if called with args, break them out - cell, tooltip, x, y = args - - # if it's the same cell as previously shown, just return - if self.__save == cell: - return - - if self.__next: # if we have something queued up, cancel it - gobject.source_remove(self.__next) - self.__next = None - - if cell: # if there was a request - if self.__shown: # if tooltip is already shown show the new one - self.__show(tooltip, x, y) - else: # else queue it up in 1/2 second - self.__next = gobject.timeout_add(500, self.__show, - tooltip, x, y) - self.__save = cell # save this cell - - def __on_expose_event(self, window, event): - ''' - this magic is required so the window appears with a 1-pixel - black border (default gtk Style). - ''' - w, h = window.size_request() - window.style.paint_flat_box(window.window, gtk.STATE_NORMAL, - gtk.SHADOW_OUT, None, window, 'tooltip', 0, 0, w, h) - - def add_view(self, view): - ''' - add a gtk.TreeView to the tooltip - ''' - view.connect('motion-notify-event', self.__motion_handler) - view.connect('leave-notify-event', lambda i, j: self.__hide()) - - def get_tooltip(self, view, column, path): - ''' - See the module doc string for a description of this method - ''' - if column is self.namecol: - model = view.get_model() - name = model[path][0] - return name.file.filename - - -if __name__ == '__main__': - #Translations - t = gettext.translation('gui', 'locale', fallback=True) - _ = t.ugettext - t.install() - - #Main - GUI() - gtk.main() diff --git a/test/clitest.py b/test/clitest.py index 781fb80..c2d11af 100644 --- a/test/clitest.py +++ b/test/clitest.py @@ -18,14 +18,14 @@ class TestRemovecli(test.MATTest): def test_remove(self): '''make sure that the cli remove all compromizing meta''' for _, dirty in self.file_list: - subprocess.call(['../cli.py', dirty]) + subprocess.call(['../mat-cli.py', dirty]) current_file = mat.create_class_file(dirty, False, True) self.assertTrue(current_file.is_clean()) def test_remove_empty(self): '''Test removal with clean files''' for clean, _ in self.file_list: - subprocess.call(['../cli.py', clean]) + subprocess.call(['../mat-cli.py', clean]) current_file = mat.create_class_file(clean, False, True) self.assertTrue(current_file.is_clean()) @@ -37,7 +37,7 @@ class TestListcli(test.MATTest): def test_list_clean(self): '''check if get_meta returns meta''' for clean, _ in self.file_list: - proc = subprocess.Popen(['../cli.py', '-d', clean], + proc = subprocess.Popen(['../mat-cli.py', '-d', clean], stdout=subprocess.PIPE) stdout, _ = proc.communicate() self.assertEqual(stdout.strip('\n'), "[+] File %s :\nNo harmful \ @@ -46,7 +46,7 @@ meta found" % clean) def test_list_dirty(self): '''check if get_meta returns all the expected meta''' for _, dirty in self.file_list: - proc = subprocess.Popen(['../cli.py', '-d', dirty], + proc = subprocess.Popen(['../mat-cli.py', '-d', dirty], stdout=subprocess.PIPE) stdout, _ = proc.communicate() self.assertNotEqual(stdout, "[+] File %s" % dirty) @@ -59,7 +59,7 @@ class TestisCleancli(test.MATTest): def test_clean(self): '''test is_clean on clean files''' for clean, _ in self.file_list: - proc = subprocess.Popen(['../cli.py', '-c', clean], + proc = subprocess.Popen(['../mat-cli.py', '-c', clean], stdout=subprocess.PIPE) stdout, _ = proc.communicate() self.assertEqual(stdout.strip('\n'), '[+] %s is clean' % clean) @@ -67,7 +67,7 @@ class TestisCleancli(test.MATTest): def test_dirty(self): '''test is_clean on dirty files''' for _, dirty in self.file_list: - proc = subprocess.Popen(['../cli.py', '-c', dirty], + proc = subprocess.Popen(['../mat-cli.py', '-c', dirty], stdout=subprocess.PIPE) stdout, _ = proc.communicate() self.assertEqual(stdout.strip('\n'), '[+] %s is not clean' % dirty) -- cgit v1.3