mirror of
https://github.com/volatilityfoundation/volatility3.git
synced 2026-08-21 05:52:22 +02:00
Add in FormatSpecification object, and bulk up the TextRenderer.
This commit is contained in:
@@ -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.")
|
||||
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<fill>.)?(?P<align>[<>=^]))?" +
|
||||
"(?P<sign>[ +-])?" +
|
||||
"(?P<alt>#)?" +
|
||||
"?(?P<zero>0)?" +
|
||||
"(?P<width>[0-9]+)?" +
|
||||
"(?P<precision>[.][0-9]+)?" +
|
||||
"(?P<type>[" + 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
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user