# Code to handle displaying and logging of results. # Anything in binwalk that prints results to screen should use this class. import sys import csv as pycsv import datetime import binwalk.core.common from binwalk.core.compat import * class Display(object): ''' Class to handle display of output and writing to log files. This class is instantiated for all modules implicitly and should not need to be invoked directly by most modules. ''' SCREEN_WIDTH = 0 HEADER_WIDTH = 80 DEFAULT_FORMAT = "%s\n" def __init__(self, quiet=False, verbose=False, log=None, csv=False, fit_to_screen=False): self.quiet = quiet self.verbose = verbose self.fit_to_screen = fit_to_screen self.fp = None self.csv = None self.num_columns = 0 self.custom_verbose_format = "" self.custom_verbose_args = [] self._configure_formatting() if log: self.fp = open(log, "a") if csv: self.csv = pycsv.writer(self.fp) def _fix_unicode(self, line): ''' This is a hack, there must be a better way to handle it. In Python3, if the environment variable LANG=C is set, indicating that the terminal is ASCII only, but unicode characters need to be printed to the screen or to a file (e.g., file path, magic result format string), then an UnicodeEncodError exception will be raised. This converts the given line to ASCII, ignoring conversion errors, and returns a str. ''' return bytes2str(line.encode('ascii', 'ignore')) def _fix_unicode_list(self, columns): ''' Convenience wrapper for self.log which is passed a list of format arguments. ''' if type(columns) in [list, tuple]: for i in range(0, len(columns)): try: columns[i] = self._fix_unicode(columns[i]) except AttributeError: pass return columns def format_strings(self, header, result): self.result_format = result self.header_format = header if self.num_columns == 0: self.num_columns = len(header.split()) def log(self, fmt, columns): if self.fp: if self.csv: try: self.csv.writerow(columns) except UnicodeEncodeError: self.csv.writerow(self._fix_unicode_list(columns)) else: try: self.fp.write(fmt % tuple(columns)) except UnicodeEncodeError: self.fp.write(fmt % tuple(self._fix_unicode_list(columns))) self.fp.flush() def add_custom_header(self, fmt, args): self.custom_verbose_format = fmt self.custom_verbose_args = args def header(self, *args, **kwargs): file_name = None self.num_columns = len(args) if has_key(kwargs, 'file_name'): file_name = kwargs['file_name'] if self.verbose and file_name: md5sum = binwalk.core.common.file_md5(file_name) timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") if self.csv: self.log("", ["FILE", "MD5SUM", "TIMESTAMP"]) self.log("", [file_name, md5sum, timestamp]) self._fprint("%s", "\n", csv=False) self._fprint("Scan Time: %s\n", [ timestamp], csv=False, filter=False) self._fprint("Target File: %s\n", [ file_name], csv=False, filter=False) self._fprint( "MD5 Checksum: %s\n", [md5sum], csv=False, filter=False) if self.custom_verbose_format and self.custom_verbose_args: self._fprint(self.custom_verbose_format, self.custom_verbose_args, csv=False, filter=False) self._fprint("%s", "\n", csv=False, filter=False) self._fprint(self.header_format, args, filter=False) self._fprint("%s", ["-" * self.HEADER_WIDTH + "\n"], csv=False, filter=False) def result(self, *args): # Convert to list for item assignment args = list(args) # Replace multiple spaces with single spaces. This is to prevent accidentally putting # four spaces in the description string, which would break # auto-formatting. for i in range(len(args)): if isinstance(args[i], str): while " " in args[i]: args[i] = args[i].replace(" ", " ") self._fprint(self.result_format, tuple(args)) def footer(self): self._fprint("%s", "\n", csv=False, filter=False) def _fprint(self, fmt, columns, csv=True, stdout=True, filter=True): line = fmt % tuple(columns) if not self.quiet and stdout: try: try: sys.stdout.write(self._format_line(line.strip()) + "\n") except UnicodeEncodeError: line = self._fix_unicode(line) sys.stdout.write(self._format_line(line.strip()) + "\n") sys.stdout.flush() except IOError as e: pass if self.fp and not (self.csv and not csv): self.log(fmt, columns) def _append_to_data_parts(self, data, start, end): ''' Intelligently appends data to self.string_parts. For use by self._format. ''' try: while data[start] == ' ': start += 1 if start == end: end = len(data[start:]) self.string_parts.append(data[start:end]) except KeyboardInterrupt as e: raise e except Exception: try: self.string_parts.append(data[start:]) except KeyboardInterrupt as e: raise e except Exception: pass return start def _format_line(self, line): ''' Formats a line of text to fit in the terminal window. For Tim. ''' delim = '\n' offset = 0 self.string_parts = [] # Split the line into an array of columns, e.g., ['0', '0x00000000', # 'Some description here'] line_columns = line.split(None, self.num_columns - 1) if line_columns: # Find where the start of the last column (description) starts in the line of text. # All line wraps need to be aligned to this offset. offset = line.rfind(line_columns[-1]) # The delimiter will be a newline followed by spaces padding out # the line wrap to the alignment offset. delim += ' ' * offset if line_columns and self.fit_to_screen and len(line) > self.SCREEN_WIDTH: # Calculate the maximum length that each wrapped line can be max_line_wrap_length = self.SCREEN_WIDTH - offset # Append all but the last column to formatted_line formatted_line = line[:offset] # Loop to split up line into multiple max_line_wrap_length pieces while len(line[offset:]) > max_line_wrap_length: # Find the nearest space to wrap the line at (so we don't split # a word across two lines) split_offset = line[ offset:offset + max_line_wrap_length].rfind(' ') # If there were no good places to split the line, just truncate # it at max_line_wrap_length if split_offset < 1: split_offset = max_line_wrap_length self._append_to_data_parts(line, offset, offset + split_offset) offset += split_offset # Add any remaining data (guarunteed to be max_line_wrap_length # long or shorter) to self.string_parts self._append_to_data_parts(line, offset, offset + len(line[offset:])) # Append self.string_parts to formatted_line; each part seperated # by delim formatted_line += delim.join(self.string_parts) else: formatted_line = line return formatted_line def _configure_formatting(self): ''' Configures output formatting, and fitting output to the current terminal width. Returns None. ''' self.format_strings(self.DEFAULT_FORMAT, self.DEFAULT_FORMAT) if self.fit_to_screen: try: import fcntl import struct import termios # Get the terminal window width hw = struct.unpack('hh', fcntl.ioctl(1, termios.TIOCGWINSZ, '1234')) self.SCREEN_WIDTH = self.HEADER_WIDTH = hw[1] except KeyboardInterrupt as e: raise e except Exception: pass