Skip to content

Commit 377697c

Browse files
authored
Merge pull request #1622 from volatilityfoundation/feature/data-renderer
Feature/data renderer
2 parents e91dfb2 + e86981c commit 377697c

File tree

12 files changed

+360
-107
lines changed

12 files changed

+360
-107
lines changed

volatility3/cli/text_renderer.py

Lines changed: 110 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
import string
1010
import sys
1111
from functools import wraps
12-
from typing import Any, Callable, Dict, List, Tuple
12+
from typing import Any, Callable, Dict, List, Tuple, TypeVar, Union
1313
from volatility3.cli import text_filter
1414

1515
from volatility3.framework import exceptions, interfaces, renderers
16+
from volatility3.framework.interfaces.renderers import BaseAbsentValue
1617
from volatility3.framework.renderers import format_hints
1718

1819
vollog = logging.getLogger(__name__)
@@ -80,7 +81,10 @@ def multitypedata_as_text(value: format_hints.MultiTypeData) -> str:
8081
return hex_bytes_as_text(value)
8182

8283

83-
def optional(func: Callable) -> Callable:
84+
T = TypeVar("T")
85+
86+
87+
def optional(func: Callable[[Union[BaseAbsentValue, T]], str]) -> Callable[[T], str]:
8488
@wraps(func)
8589
def wrapped(x: Any) -> str:
8690
if isinstance(x, interfaces.renderers.BaseAbsentValue):
@@ -110,7 +114,7 @@ def wrapped(x: Any) -> str:
110114
return wrapped
111115

112116

113-
def display_disassembly(disasm: interfaces.renderers.Disassembly) -> str:
117+
def display_disassembly(disasm: renderers.Disassembly) -> str:
114118
"""Renders a disassembly renderer type into string format.
115119
116120
Args:
@@ -137,9 +141,104 @@ def display_disassembly(disasm: interfaces.renderers.Disassembly) -> str:
137141
return QuickTextRenderer._type_renderers[bytes](disasm.data)
138142

139143

144+
class CLITypeRenderer(interfaces.renderers.TypeRendererInterface):
145+
def __init__(self, func):
146+
super().__init__(func=optional(func))
147+
148+
149+
class LayerDataRenderer(CLITypeRenderer):
150+
"""Renders a LayerData object into data/bytes"""
151+
152+
def __init__(self):
153+
self.context_byte_len = 0
154+
self.width = 16
155+
self.display_offset = False
156+
self.display_hex = True
157+
self.display_ascii = True
158+
159+
def render(data: Union[renderers.LayerData, BaseAbsentValue]):
160+
if isinstance(data, BaseAbsentValue):
161+
# FIXME: Do something cleverer here
162+
return ""
163+
164+
context_byte_len = self.context_byte_len if not data.no_surrounding else 0
165+
166+
layer = data.context.layers[data.layer_name]
167+
# Map of the holes
168+
error_bytes = set()
169+
start_offset = data.offset - context_byte_len
170+
end_offset = data.offset + data.length + context_byte_len
171+
if isinstance(layer, interfaces.layers.TranslationLayerInterface):
172+
error_bytes = set()
173+
mapping = iter(layer.mapping(start_offset, end_offset, True))
174+
current_map = next(mapping)
175+
for i in range(start_offset, end_offset):
176+
# Run through the bytes, check if they're present
177+
offset, sublength, _, _, _ = current_map
178+
if i < offset:
179+
error_bytes.add(i - start_offset)
180+
if i > offset + sublength:
181+
try:
182+
current_map = next(mapping)
183+
except StopIteration:
184+
pass
185+
offset, sublength, _, _, _ = current_map
186+
if i > offset + sublength:
187+
error_bytes.add(i - start_offset)
188+
189+
# Padded data
190+
specific_data = data.context.layers[data.layer_name].read(
191+
start_offset,
192+
end_offset - start_offset,
193+
True,
194+
)
195+
196+
printables = ""
197+
output = "\n"
198+
for count, byte in enumerate(specific_data):
199+
if count not in error_bytes:
200+
output += f"{byte:02x} "
201+
char = chr(byte)
202+
printables += char if 0x20 <= byte <= 0x7E else "."
203+
else:
204+
output += "__ "
205+
printables += "."
206+
if count % self.width == self.width - 1:
207+
output += printables
208+
if count < len(specific_data) - 1:
209+
output += "\n"
210+
printables = ""
211+
212+
# Handle leftovers when the length is not mutiple of width
213+
if printables:
214+
padding = self.width - len(printables)
215+
output += " " * padding
216+
output += printables
217+
output += " " * padding
218+
219+
return output
220+
221+
render_func = render
222+
return super().__init__(render_func)
223+
224+
140225
class CLIRenderer(interfaces.renderers.Renderer):
141226
"""Class to add specific requirements for CLI renderers."""
142227

228+
_type_renderers = {
229+
format_hints.Bin: CLITypeRenderer(lambda x: f"0b{x:b}"),
230+
format_hints.Hex: CLITypeRenderer(lambda x: f"0x{x:x}"),
231+
format_hints.HexBytes: CLITypeRenderer(hex_bytes_as_text),
232+
format_hints.MultiTypeData: CLITypeRenderer(multitypedata_as_text),
233+
renderers.Disassembly: CLITypeRenderer(display_disassembly),
234+
bytes: CLITypeRenderer(lambda x: " ".join(f"{b:02x}" for b in x)),
235+
renderers.LayerData: LayerDataRenderer(),
236+
datetime.datetime: CLITypeRenderer(
237+
lambda x: x.strftime("%Y-%m-%d %H:%M:%S.%f %Z")
238+
),
239+
"default": CLITypeRenderer(lambda x: f"{x}"),
240+
}
241+
143242
name = "unnamed"
144243
structured_output = False
145244
filter: text_filter.CLIFilter = None
@@ -170,21 +269,11 @@ def ignored_columns(
170269

171270

172271
class QuickTextRenderer(CLIRenderer):
173-
_type_renderers = {
174-
format_hints.Bin: optional(lambda x: f"0b{x:b}"),
175-
format_hints.Hex: optional(lambda x: f"0x{x:x}"),
176-
format_hints.HexBytes: optional(hex_bytes_as_text),
177-
format_hints.MultiTypeData: quoted_optional(multitypedata_as_text),
178-
interfaces.renderers.Disassembly: optional(display_disassembly),
179-
bytes: optional(lambda x: " ".join(f"{b:02x}" for b in x)),
180-
datetime.datetime: optional(lambda x: x.strftime("%Y-%m-%d %H:%M:%S.%f %Z")),
181-
"default": optional(lambda x: f"{x}"),
182-
}
183272

184273
name = "quick"
185274

186275
def get_render_options(self):
187-
pass
276+
return []
188277

189278
def render(self, grid: interfaces.renderers.TreeGrid) -> None:
190279
"""Renders each column immediately to stdout.
@@ -242,30 +331,20 @@ class NoneRenderer(CLIRenderer):
242331
name = "none"
243332

244333
def get_render_options(self):
245-
pass
334+
return []
246335

247336
def render(self, grid: interfaces.renderers.TreeGrid) -> None:
248337
if not grid.populated:
249338
grid.populate(lambda x, y: True, True)
250339

251340

252341
class CSVRenderer(CLIRenderer):
253-
_type_renderers = {
254-
format_hints.Bin: optional(lambda x: f"0b{x:b}"),
255-
format_hints.Hex: optional(lambda x: f"0x{x:x}"),
256-
format_hints.HexBytes: optional(hex_bytes_as_text),
257-
format_hints.MultiTypeData: optional(multitypedata_as_text),
258-
interfaces.renderers.Disassembly: optional(display_disassembly),
259-
bytes: optional(lambda x: " ".join(f"{b:02x}" for b in x)),
260-
datetime.datetime: optional(lambda x: x.strftime("%Y-%m-%d %H:%M:%S.%f %Z")),
261-
"default": optional(lambda x: f"{x}"),
262-
}
263342

264343
name = "csv"
265344
structured_output = True
266345

267346
def get_render_options(self):
268-
pass
347+
return []
269348

270349
def render(self, grid: interfaces.renderers.TreeGrid) -> None:
271350
"""Renders each row immediately to stdout.
@@ -316,12 +395,10 @@ def visitor(node: interfaces.renderers.TreeNode, accumulator):
316395

317396

318397
class PrettyTextRenderer(CLIRenderer):
319-
_type_renderers = QuickTextRenderer._type_renderers
320-
321398
name = "pretty"
322399

323400
def get_render_options(self):
324-
pass
401+
return []
325402

326403
def render(self, grid: interfaces.renderers.TreeGrid) -> None:
327404
"""Renders each column immediately to stdout.
@@ -380,7 +457,7 @@ def visitor(
380457
accumulator.append((node.path_depth, line))
381458
return accumulator
382459

383-
final_output: List[Tuple[int, Dict[interfaces.renderers.Column, bytes]]] = []
460+
final_output: List[Tuple[int, Dict[interfaces.renderers.Column, str]]] = []
384461
if not grid.populated:
385462
grid.populate(visitor, final_output)
386463
else:
@@ -417,9 +494,7 @@ def visitor(
417494
if column in ignore_columns:
418495
del line[column]
419496
else:
420-
line[column] = line[column] + (
421-
[""] * (nums_line - len(line[column]))
422-
)
497+
line[column] = line[column] + ("" * (nums_line - len(line[column])))
423498
for index in range(nums_line):
424499
if index == 0:
425500
outfd.write(
@@ -448,7 +523,7 @@ def tab_stop(self, line: str) -> str:
448523
class JsonRenderer(CLIRenderer):
449524
_type_renderers = {
450525
format_hints.HexBytes: quoted_optional(hex_bytes_as_text),
451-
interfaces.renderers.Disassembly: quoted_optional(display_disassembly),
526+
renderers.Disassembly: quoted_optional(display_disassembly),
452527
format_hints.MultiTypeData: quoted_optional(multitypedata_as_text),
453528
bytes: optional(lambda x: " ".join(f"{b:02x}" for b in x)),
454529
datetime.datetime: lambda x: (
@@ -463,7 +538,7 @@ class JsonRenderer(CLIRenderer):
463538
structured_output = True
464539

465540
def get_render_options(self) -> List[interfaces.renderers.RenderOption]:
466-
pass
541+
return []
467542

468543
def output_result(self, outfd, result):
469544
"""Outputs the JSON data to a file in a particular format"""

volatility3/framework/constants/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# We use the SemVer 2.0.0 versioning scheme
22
VERSION_MAJOR = 2 # Number of releases of the library with a breaking change
3-
VERSION_MINOR = 25 # Number of changes that only add to the interface
3+
VERSION_MINOR = 26 # Number of changes that only add to the interface
44
VERSION_PATCH = 0 # Number of changes that do not change the interface
55
VERSION_SUFFIX = ""
66

0 commit comments

Comments
 (0)