From 2e5656ade430eae76152d70a080b2d5fd34c138b Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 14 Apr 2014 02:22:05 +0100 Subject: [PATCH] Add in FormatSpecification object, and bulk up the TextRenderer. --- volatility/framework/renderers/__init__.py | 236 ++++++++++++++++++++- volatility/framework/renderers/basic.py | 47 ++-- 2 files changed, 260 insertions(+), 23 deletions(-) diff --git a/volatility/framework/renderers/__init__.py b/volatility/framework/renderers/__init__.py index 99aa8704a..f36252265 100644 --- a/volatility/framework/renderers/__init__.py +++ b/volatility/framework/renderers/__init__.py @@ -1,9 +1,16 @@ +import copy +import collections + __author__ = 'mike' +import re + from volatility.framework import validity + class TreeRow(validity.ValidityRoutines): """Class providing the interface for an individual Row of the TreeGrid""" + def __init__(self, treegrid, values): self.type_check(treegrid, TreeGrid) if not isinstance(self, TreeGrid): @@ -30,6 +37,10 @@ class TreeRow(validity.ValidityRoutines): """ self._children = [] + @property + def values(self): + return copy.deepcopy(self._values) + @property def children(self): """Returns an iterator of the children of the current row @@ -50,20 +61,26 @@ class TreeRow(validity.ValidityRoutines): for grandchild in child.iterator(level + 1): yield grandchild + +Column = collections.namedtuple('Column', ['index', 'name', 'type', 'format']) + + class TreeGrid(TreeRow): """Class providing the interface for a TreeGrid (which contains TreeRows)""" - simple_types = {int, str, float, bytes} - column_formats = {"address"} + simple_types = set((int, str, float, bytes)) def __init__(self, columns): """Constructs a TreeGrid object using a specific set of columns - The TreeGrid itself is a root element, that can have children but no values + The TreeGrid itself is a root element, that can have children but no values. + The format_hint is a suggestion to the renderer as to how the field should be portrayed as a string, + but it should be noted that the renderer is not under obligation to use it. - :param columns: A list of column tuples made up of (name, type and formatter). + :param columns: A list of column tuples made up of (name, type and format_hint). """ self.type_check(columns, list) + converted_columns = [] for (name, column_type, column_format) in columns: is_simple_type = False for t in self.simple_types: @@ -71,15 +88,214 @@ class TreeGrid(TreeRow): if not is_simple_type: raise TypeError("Column " + name + "'s type " + column_type.__class__.__name__ + " is not a simple type") - if not column_format is None and not column_format in self.column_formats: - raise TypeError("Column " + name + "'s format " + column_format + " is not an accepted formatter.") - self._columns = columns + if isinstance(column_format, str): + column_format = FormatSpecification.from_specification(column_format) + if not (column_format is None or isinstance(column_format, FormatSpecification)): + raise TypeError( + "Column " + name + "'s format " + repr(column_format) + " is not an accepted formatter.") + converted_columns.append(Column(len(converted_columns), name, column_type, column_format)) + self._columns = converted_columns # We can use the special type None because we're the top level node without values TreeRow.__init__(self, self, None) + @property + def columns(self): + """Returns list of tuples of (name, type and format_hint)""" + for column in self._columns: + yield column + def validate_values(self, values): """Takes a list of values and verified them against the column types""" - for i in range(len(self._columns)): - if not isinstance(values[i], self._columns[i]): - raise TypeError("Column type " + str(i) + " is incorrect.") \ No newline at end of file + if len(values) != len(self._columns): + raise ValueError("The length of the values provided does not match the number of columns.") + for column in self._columns: + if not isinstance(values[column.index], column.type): + raise TypeError("Column type " + str(column.index) + " is incorrect.") + + def iterator(self, level = 0): + """Returns an iterator of all rows with their depths + + :type level: int + :param level: Indicates the depth of the current iterator + """ + for child in self.children: + for grandchild in child.iterator(level + 1): + yield grandchild + + +class FormatSpecification(object): + valid_types = "bcdeEfFgGnosxX%" + pattern = re.compile("^((?P.)?(?P[<>=^]))?" + + "(?P[ +-])?" + + "(?P#)?" + + "?(?P0)?" + + "(?P[0-9]+)?" + + "(?P[.][0-9]+)?" + + "(?P[" + valid_types + "])?$") + + # noinspection PyShadowingBuiltins + def __init__(self, fill = None, align = None, sign = None, alt = None, zero = None, width = None, + precision = None, type = None): + self._fill = fill + self._align = align + self._sign = sign + self._alt = alt + self._zero = zero + self._width = width + self._precision = precision + self._type = type + + @classmethod + def from_specification(cls, format_spec): + """Converts a format_specification string into a FormatSpecification object + + :param format_spec: Format specification string + :type format_spec: str + + :rtype: FormatSpecification + :return: A FormatSpecification object with the various parameters parsed + """ + result = cls.pattern.match(format_spec) + if not result: + raise ValueError("Invalid format specification identified.") + return cls(result.group('fill'), + result.group('align'), + result.group('sign'), + bool(result.group('alt')), + bool(result.group('zero')), + int(result.group('width')) if result.group('width') else None, + int(result.group('precision')[1:]) if result.group('precision') else None, + result.group('type')) + + @property + def fill(self): + """Selects a character to fill out the empty space after a specific alignment has been chosen.""" + return self._fill + + @property + def align(self): + """Determines how to align the text + + '>' will align to the right (default for numbers) + '<' will align to the left (default for strings) + '^' will align to the center + '=' is only valid for numeric types and will force the padding to go after the sign but before digits + + The alignment option has no meaning if the width is not specified, as it will default to the same width as the + data. + """ + return self._align + + @property + def sign(self): + """Determines whether to insert a sign + + '+' indicates a sign should be present on both positive and negative numbers + '-' indicates that a sign should be present only on negative numbers (the default) + ' ' indicates that a leading space should be used on positive numbers, and a minus sign on negative ones + """ + return self._sign + + @property + def alt(self): + """Specifies whether an alternate form should be used. + + This option is only valid for integer, float, complex and Decimal types. For integers, when binary, octal, or + hexadecimal output is used, this option adds the prefix respective '0b', '0o', or '0x' to the output value. For + floats, complex and Decimal the alternate form causes the result of the conversion to always contain a + decimal-point character, even if no digits follow it. Normally, a decimal-point character appears in the result + of these conversions only if a digit follows it. In addition, for 'g' and 'G' conversions, trailing zeros are + not removed from the result. + """ + return self._alt + + @property + def zero(self): + """Specifies whether to enable sign-aware zero-padding for numeric types. + + This is equivalent to a fill character of '0' with an alignment type of '='. + """ + return self._zero + + @property + def width(self): + """Specifies a decimal integer defining the minimum field width. + + If not specified, then the field width will be determined by the content. + """ + return self._width + + @property + def precision(self): + """Specifies a decimal number indicating how many digits after the decimal point should be displayed. + + This specifies a decimal number indicating how many digits should be displayed after the decimal point for a + floating point value formatted with 'f' and 'F', or before and after the decimal point for a floating point + value formatted with 'g' or 'G'. For non-number types the field indicates the maximum field size - in other + words, how many characters will be used from the field content. The precision is not allowed for integer values. + """ + return self._precision + + @property + def type(self): + """Specifies the type conversion + + For complete documentation, see the Format Specification Mini-Language in the python string documentation. + """ + return self._type + + @fill.setter + def fill(self, value): + if value and not (isinstance(value, str) and len(value) == 1): + raise ValueError("Fill value must be a single character string.") + self._fill = value or None + + @align.setter + def align(self, value): + if value and not (value in "<>^=" and len(value) == 1): + raise ValueError("Alignment value must be one of '<', '>', '=' or '^', not '" + value + "'.") + self._align = value or None + + @sign.setter + def sign(self, value): + if value and not (value in "" and len(value) == 1): + raise ValueError("Sign value must be one of '-', '+' or ' ', not '" + value + "'.") + self._sign = value or None + + @alt.setter + def alt(self, value): + self._alt = bool(value) + + @zero.setter + def zero(self, value): + self._zero = bool(value) + + @width.setter + def width(self, value): + self._width = int(value) + + @precision.setter + def precision(self, value): + self._precision = int(value) + + @type.setter + def type(self, value): + if not (value in self.valid_types and len(value) == 1): + raise ValueError("Invalid type value provided.") + self._type = value + + def to_string(self): + return str(self) + + def __str__(self): + """""" + spec = ((self.fill if self.fill and self.align else '') + + (self.align or '') + + (self.sign or '') + + ('#' if self.alt else '') + + ('0' if self.zero else '') + + (str(self.width) if self.width else '') + + (("." + str(self.precision)) if self.precision or self.precision == 0 else '') + + (self.type or '')) + return spec \ No newline at end of file diff --git a/volatility/framework/renderers/basic.py b/volatility/framework/renderers/basic.py index 4c61d3173..032cb149e 100644 --- a/volatility/framework/renderers/basic.py +++ b/volatility/framework/renderers/basic.py @@ -1,32 +1,53 @@ +from framework.renderers import FormatSpecification + __author__ = 'mike' from volatility.framework.interfaces import renderers as interface from volatility.framework import renderers +import sys + class TextRenderer(interface.Renderer): + def get_render_options(self): + # FIXME: Fill in the docstring and provide render_options + pass def __init__(self, options): interface.Renderer.__init__(self, options) self._options = options - self._headers = [] + self._column_widths = [] def render(self, grid): """Renders a text grid based on the contents of each element""" - # Render headers and calculate column widths self.type_check(grid, renderers.TreeGrid) - grid.iterator() - def _determine_headers(self, grid): - self.type_check(grid, renderers.TreeGrid) - self._headers = [] + # FIXME: Separator should come from options + sep = " | " + + max_level = -1 + column_maximum_widths = [max(len(column.name), column.format.width) for column in grid.columns] + for (level, row) in grid.iterator(): + max_level = max(max_level, level) + column_maximum_widths = [max(column_maximum_widths[column.index], len( + ("{0:" + column.format.to_string() + "}").format(row.values[column.index]))) for column in grid.columns] + for column in grid.columns: - self._headers.append((column.name, len(column.name))) + column.format.width = column_maximum_widths[column.index] + column.format.fill = ' ' + # column.format.align = '<' + # Run through the values and determine their maximum lengths, perhaps build up a two dimensional array + # Then print out the headers and the values at their appropriate spacings + # Potentially warn if the output is likely to be longer than the display area. - def render_row(self, row, level): - pass + headers = [("{0:" + FormatSpecification(width = column_maximum_widths[column.index], fill = ' ', + align = '^').to_string() + "}").format(column.name) for column in + grid.columns] + print(sep.join(headers)) - def _subrender(self, subgrid, level): - for child in subgrid: - self.render_row(child, level + 1) - self._subrender(child, level + 1) + for (level, row) in grid.iterator(): + row_text = [] + for column in grid.columns: + row_text.append(("{:" + column.format.to_string() + "}").format(row.values[column.index])) + line = sep.join(row_text) + sys.stdout.write(line + "\n") \ No newline at end of file