From d9ad643737ec9707e0d410da80f67a3523113228 Mon Sep 17 00:00:00 2001 From: Donghyun Kim Date: Fri, 13 May 2022 02:41:43 +0900 Subject: [PATCH 1/8] Add: mermaid renderer initialize code --- volatility3/cli/text_renderer.py | 51 ++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 08608a3d0..1ac27965d 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -391,3 +391,54 @@ class JsonLinesRenderer(JsonRenderer): for line in result: outfd.write(json.dumps(line, sort_keys = True)) outfd.write("\n") + +class MermaidRenderer(CLIRenderer): + _type_renderers = { + format_hints.Bin: optional(lambda x: f"0b{x:b}"), + format_hints.Hex: optional(lambda x: f"0x{x:x}"), + format_hints.HexBytes: optional(hex_bytes_as_text), + format_hints.MultiTypeData: optional(multitypedata_as_text), + interfaces.renderers.Disassembly: optional(display_disassembly), + bytes: optional(lambda x: " ".join([f"{b:02x}" for b in x])), + datetime.datetime: optional(lambda x: x.strftime("%Y-%m-%d %H:%M:%S.%f %Z")), + 'default': optional(lambda x: f"{x}") + } + + name = "csv" + structured_output = True + + def get_render_options(self): + pass + + def render(self, grid: interfaces.renderers.TreeGrid) -> None: + """Renders each row immediately to stdout. + + Args: + grid: The TreeGrid object to render + """ + outfd = sys.stdout + + header_list = ['TreeDepth'] + for column in grid.columns: + # Ignore the type because namedtuples don't realize they have accessible attributes + header_list.append(f"{column.name}") + + writer = csv.DictWriter(outfd, header_list) + writer.writeheader() + + def visitor(node: interfaces.renderers.TreeNode, accumulator): + # Nodes always have a path value, giving them a path_depth of at least 1, we use max just in case + row = {'TreeDepth': str(max(0, node.path_depth - 1))} + for column_index in range(len(grid.columns)): + column = grid.columns[column_index] + renderer = self._type_renderers.get(column.type, self._type_renderers['default']) + row[f'{column.name}'] = renderer(node.values[column_index]) + accumulator.writerow(row) + return accumulator + + if not grid.populated: + grid.populate(visitor, writer) + else: + grid.visit(node = None, function = visitor, initial_accumulator = writer) + + outfd.write("\n") From 671b3db01a795fd8ab4faabf8b476655ddfd3145 Mon Sep 17 00:00:00 2001 From: Donghyun Kim Date: Sun, 15 May 2022 00:14:34 +0900 Subject: [PATCH 2/8] Add: mermaid branch, node formatting logic --- volatility3/cli/text_renderer.py | 75 +++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 15 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 1ac27965d..6b082b7c7 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -404,41 +404,86 @@ class MermaidRenderer(CLIRenderer): 'default': optional(lambda x: f"{x}") } - name = "csv" + name = "mermaid" structured_output = True + # Parents or PID PPID 존재할 시에만 가능 + def get_render_options(self): pass def render(self, grid: interfaces.renderers.TreeGrid) -> None: - """Renders each row immediately to stdout. + """Renders each column immediately to stdout. + + This does not format each line's width appropriately, it merely tab separates each field Args: grid: The TreeGrid object to render """ outfd = sys.stdout - header_list = ['TreeDepth'] - for column in grid.columns: - # Ignore the type because namedtuples don't realize they have accessible attributes - header_list.append(f"{column.name}") + sys.stderr.write("Formatting...\n") - writer = csv.DictWriter(outfd, header_list) - writer.writeheader() + display_alignment = ">" + column_separator = " | " - def visitor(node: interfaces.renderers.TreeNode, accumulator): + tree_indent_column = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(20)) + max_column_widths = dict([(column.name, len(column.name)) for column in grid.columns]) + + def visitor( + node: interfaces.renderers.TreeNode, + accumulator: List[Tuple[int, Dict[interfaces.renderers.Column, bytes]]] + ) -> List[Tuple[int, Dict[interfaces.renderers.Column, bytes]]]: # Nodes always have a path value, giving them a path_depth of at least 1, we use max just in case - row = {'TreeDepth': str(max(0, node.path_depth - 1))} + max_column_widths[tree_indent_column] = max(max_column_widths.get(tree_indent_column, 0), node.path_depth) + line = {} for column_index in range(len(grid.columns)): column = grid.columns[column_index] renderer = self._type_renderers.get(column.type, self._type_renderers['default']) - row[f'{column.name}'] = renderer(node.values[column_index]) - accumulator.writerow(row) + data = renderer(node.values[column_index]) + field_width = max([len(self.tab_stop(x)) for x in f"{data}".split("\n")]) + max_column_widths[column.name] = max(max_column_widths.get(column.name, len(column.name)), + field_width) + line[column] = data.split("\n") + accumulator.append((node.path_depth, line)) return accumulator + final_output: List[Tuple[int, Dict[interfaces.renderers.Column, bytes]]] = [] if not grid.populated: - grid.populate(visitor, writer) + grid.populate(visitor, final_output) else: - grid.visit(node = None, function = visitor, initial_accumulator = writer) + grid.visit(node = None, function = visitor, initial_accumulator = final_output) - outfd.write("\n") + # Always align the tree to the left + format_string_list = ["{0:<" + str(max_column_widths.get(tree_indent_column, 0)) + "s}"] + for column_index in range(len(grid.columns)): + column = grid.columns[column_index] + format_string_list.append("{" + str(column_index + 1) + ":" + display_alignment + + str(max_column_widths[column.name]) + "s}") + + format_string = column_separator.join(format_string_list) + "\n" + + column_titles = [""] + [column.name for column in grid.columns] + outfd.write(format_string.format(*column_titles)) + + tree_header = "graph TD\n" + branch_data = f"{tree_header}" + + own_column = ["PID"] + parent_column = ["PPID"] + + for (_, line) in final_output: + nums_line = max([len(line[column]) for column in line]) + for column in line: + line[column] = line[column] + ([""] * (nums_line - len(line[column]))) + for index in range(nums_line): + node_data = "" + for column in grid.columns: + node_data += f"{column.name}:{line[column][index]}
" + if(column.name in own_column): + own = line[column][index] + if(column.name in parent_column): + parent = line[column][index] + branch_data += f"\t{parent} --> {own}[{node_data}]\n".replace("(V)", "") + print(branch_data) + #outfd.write(format_string.format("*" * depth, *[self.tab_stop(line[column][index]) for column in grid.columns])) From 6a9bcc16a592ff3a2e9d9e965a14747599f2ee34 Mon Sep 17 00:00:00 2001 From: Donghyun Kim Date: Sun, 15 May 2022 00:36:23 +0900 Subject: [PATCH 3/8] Fix: parents relation check logic --- volatility3/cli/text_renderer.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 6b082b7c7..3c9b0d217 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -464,14 +464,18 @@ class MermaidRenderer(CLIRenderer): format_string = column_separator.join(format_string_list) + "\n" column_titles = [""] + [column.name for column in grid.columns] + + own_column = ["PID"] + parent_column = ["PPID"] + + if not((own_column in column_titles) and (parent_column in column_titles)): + raise Exception("Plugin cannot be rendered as mermaid because there is no tree relationship.") + outfd.write(format_string.format(*column_titles)) tree_header = "graph TD\n" branch_data = f"{tree_header}" - own_column = ["PID"] - parent_column = ["PPID"] - for (_, line) in final_output: nums_line = max([len(line[column]) for column in line]) for column in line: @@ -487,3 +491,11 @@ class MermaidRenderer(CLIRenderer): branch_data += f"\t{parent} --> {own}[{node_data}]\n".replace("(V)", "") print(branch_data) #outfd.write(format_string.format("*" * depth, *[self.tab_stop(line[column][index]) for column in grid.columns])) + + def tab_stop(self, line: str) -> str: + tab_width = 8 + while line.find('\t') >= 0: + i = line.find('\t') + pad = " " * (tab_width - (i % tab_width)) + line = line.replace("\t", pad, 1) + return line From ae72d6f54e0ae9935f3da5abbafc5a6d4b77081a Mon Sep 17 00:00:00 2001 From: Donghyun Kim Date: Sun, 15 May 2022 00:36:47 +0900 Subject: [PATCH 4/8] Fix: outfd result data --- volatility3/cli/text_renderer.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 3c9b0d217..8203b3673 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -407,8 +407,6 @@ class MermaidRenderer(CLIRenderer): name = "mermaid" structured_output = True - # Parents or PID PPID 존재할 시에만 가능 - def get_render_options(self): pass @@ -425,7 +423,6 @@ class MermaidRenderer(CLIRenderer): sys.stderr.write("Formatting...\n") display_alignment = ">" - column_separator = " | " tree_indent_column = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(20)) max_column_widths = dict([(column.name, len(column.name)) for column in grid.columns]) @@ -461,17 +458,13 @@ class MermaidRenderer(CLIRenderer): format_string_list.append("{" + str(column_index + 1) + ":" + display_alignment + str(max_column_widths[column.name]) + "s}") - format_string = column_separator.join(format_string_list) + "\n" - column_titles = [""] + [column.name for column in grid.columns] own_column = ["PID"] parent_column = ["PPID"] - if not((own_column in column_titles) and (parent_column in column_titles)): + if not((set(own_column).issubset(column_titles)) and (set(parent_column).issubset(column_titles))): raise Exception("Plugin cannot be rendered as mermaid because there is no tree relationship.") - - outfd.write(format_string.format(*column_titles)) tree_header = "graph TD\n" branch_data = f"{tree_header}" @@ -488,9 +481,8 @@ class MermaidRenderer(CLIRenderer): own = line[column][index] if(column.name in parent_column): parent = line[column][index] - branch_data += f"\t{parent} --> {own}[{node_data}]\n".replace("(V)", "") - print(branch_data) - #outfd.write(format_string.format("*" * depth, *[self.tab_stop(line[column][index]) for column in grid.columns])) + branch_data += f"\t{parent} --> {own}[{node_data}]\n".replace("(", "").replace(")", "") + outfd.write("{}\n".format(branch_data)) def tab_stop(self, line: str) -> str: tab_width = 8 From f07635fc5061e01663f1e5df128918b7af3e402c Mon Sep 17 00:00:00 2001 From: Donghyun Kim Date: Thu, 19 May 2022 22:09:39 +0900 Subject: [PATCH 5/8] Refactor: remove unused login --- volatility3/cli/text_renderer.py | 29 ++++------------------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 7bc889719..633283f27 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -422,42 +422,29 @@ class MermaidRenderer(CLIRenderer): sys.stderr.write("Formatting...\n") - display_alignment = ">" - - tree_indent_column = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(20)) - max_column_widths = dict([(column.name, len(column.name)) for column in grid.columns]) - + tree_indent_column = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(20)) # Tree Signature + def visitor( node: interfaces.renderers.TreeNode, accumulator: List[Tuple[int, Dict[interfaces.renderers.Column, bytes]]] ) -> List[Tuple[int, Dict[interfaces.renderers.Column, bytes]]]: # Nodes always have a path value, giving them a path_depth of at least 1, we use max just in case - max_column_widths[tree_indent_column] = max(max_column_widths.get(tree_indent_column, 0), node.path_depth) line = {} for column_index in range(len(grid.columns)): column = grid.columns[column_index] renderer = self._type_renderers.get(column.type, self._type_renderers['default']) data = renderer(node.values[column_index]) - field_width = max([len(self.tab_stop(x)) for x in f"{data}".split("\n")]) - max_column_widths[column.name] = max(max_column_widths.get(column.name, len(column.name)), - field_width) line[column] = data.split("\n") accumulator.append((node.path_depth, line)) return accumulator final_output: List[Tuple[int, Dict[interfaces.renderers.Column, bytes]]] = [] + if not grid.populated: grid.populate(visitor, final_output) else: grid.visit(node = None, function = visitor, initial_accumulator = final_output) - # Always align the tree to the left - format_string_list = ["{0:<" + str(max_column_widths.get(tree_indent_column, 0)) + "s}"] - for column_index in range(len(grid.columns)): - column = grid.columns[column_index] - format_string_list.append("{" + str(column_index + 1) + ":" + display_alignment + - str(max_column_widths[column.name]) + "s}") - column_titles = [""] + [column.name for column in grid.columns] own_column = ["PID"] @@ -469,7 +456,7 @@ class MermaidRenderer(CLIRenderer): tree_header = "graph TD\n" branch_data = f"{tree_header}" - for (_, line) in final_output: + for (_depth, line) in final_output: nums_line = max([len(line[column]) for column in line]) for column in line: line[column] = line[column] + ([""] * (nums_line - len(line[column]))) @@ -483,11 +470,3 @@ class MermaidRenderer(CLIRenderer): parent = line[column][index] branch_data += f"\t{parent} --> {own}[{node_data}]\n".replace("(", "").replace(")", "") outfd.write("{}\n".format(branch_data)) - - def tab_stop(self, line: str) -> str: - tab_width = 8 - while line.find('\t') >= 0: - i = line.find('\t') - pad = " " * (tab_width - (i % tab_width)) - line = line.replace("\t", pad, 1) - return line From 19b5e31ac02c28ca3fe184b51a87bc828f8e8cc2 Mon Sep 17 00:00:00 2001 From: Donghyun Kim Date: Tue, 26 May 2026 08:43:24 +0900 Subject: [PATCH 6/8] Make MermaidRenderer plugin-agnostic via TreeGrid path_depth Address review feedback that the renderer was tightly coupled to plugins exposing PID/PPID columns: the previous implementation looked up "PID" and "PPID" by name to build parent->child edges and raised a generic exception otherwise, which prevented any non-pstree tree plugin from being rendered as Mermaid. The relationship is already encoded in the TreeGrid -- every TreeNode carries its path_depth -- so the new render() walks the rows in traversal order and tracks ancestry with a parent stack: * descending one or more levels pushes the previously-emitted node once per level (so a level skip still produces sane pops); * ascending pops the corresponding number of levels; * the stack top is always the parent of the next emitted node, or empty for a root-level node. Each node is given a stable per-render identifier (n1, n2, ...) instead of being keyed by a column value such as PID, since PIDs are not unique across a TreeGrid and may contain characters that are unsafe in Mermaid node IDs. A small label-escaping helper replaces the previous ad-hoc string replacement of parentheses, and embedded newlines in cell renderings are folded to
so each row stays a single Mermaid node. The unused tree_indent_column placeholder (flagged by code scanning) is dropped as part of the rewrite. --- volatility3/cli/text_renderer.py | 123 +++++++++++++++++++++---------- 1 file changed, 83 insertions(+), 40 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 1b22be19f..1d5cf3f19 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -639,13 +639,37 @@ class MermaidRenderer(CLIRenderer): name = "mermaid" structured_output = True + @staticmethod + def _mermaid_label(text: str) -> str: + """Escape a value for use inside a Mermaid node label (``["..."]``). + + Double quotes terminate the label, so they must be replaced with the + Mermaid-supported entity. Newlines inside cell renderings are + converted to ``
`` so each row remains a single Mermaid node. + """ + return text.replace('"', """).replace("\n", "
") + def get_render_options(self): pass def render(self, grid: interfaces.renderers.TreeGrid) -> None: - """Renders each column immediately to stdout. + """Render the TreeGrid as a Mermaid ``graph TD`` flowchart. - This does not format each line's width appropriately, it merely tab separates each field + The renderer is plugin-agnostic: it derives the parent/child + relationship from each node's ``path_depth`` in the grid, rather + than from any particular column (such as PID/PPID). This means + any tree-shaped plugin output -- pstree, vadwalk, handles tree, + future plugins -- renders without modification. + + The algorithm maintains a parent stack while walking the rows in + traversal order: + + * descending one or more levels pushes the previously-emitted + node onto the stack (once per level descended) so it becomes + the current parent; + * ascending pops the same number of levels off the stack; + * the top of the stack is always the parent of the next emitted + node, or empty for a root-level node. Args: grid: The TreeGrid object to render @@ -654,51 +678,70 @@ class MermaidRenderer(CLIRenderer): sys.stderr.write("Formatting...\n") - tree_indent_column = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(20)) # Tree Signature - + def format_row(node: interfaces.renderers.TreeNode) -> str: + """Build a Mermaid node label from every column of ``node``.""" + cells = [] + for column_index, column in enumerate(grid.columns): + renderer = self._type_renderers.get( + column.type, self._type_renderers['default'] + ) + value = renderer(node.values[column_index]) + cells.append(f"{column.name}:{self._mermaid_label(value)}") + return "
".join(cells) + + rows: List[Tuple[int, str]] = [] + def visitor( node: interfaces.renderers.TreeNode, - accumulator: List[Tuple[int, Dict[interfaces.renderers.Column, bytes]]] - ) -> List[Tuple[int, Dict[interfaces.renderers.Column, bytes]]]: - # Nodes always have a path value, giving them a path_depth of at least 1, we use max just in case - line = {} - for column_index in range(len(grid.columns)): - column = grid.columns[column_index] - renderer = self._type_renderers.get(column.type, self._type_renderers['default']) - data = renderer(node.values[column_index]) - line[column] = data.split("\n") - accumulator.append((node.path_depth, line)) + accumulator: List[Tuple[int, str]], + ) -> List[Tuple[int, str]]: + accumulator.append((node.path_depth, format_row(node))) return accumulator - final_output: List[Tuple[int, Dict[interfaces.renderers.Column, bytes]]] = [] - if not grid.populated: - grid.populate(visitor, final_output) + grid.populate(visitor, rows) else: - grid.visit(node = None, function = visitor, initial_accumulator = final_output) + grid.visit(node=None, function=visitor, initial_accumulator=rows) - column_titles = [""] + [column.name for column in grid.columns] + # Stable, unique per-node IDs. We never reuse a column value (e.g. + # PID) because (a) PID is not guaranteed unique across a TreeGrid, + # (b) it is plugin-specific, and (c) Mermaid IDs must avoid + # characters like parentheses that may appear in column data. + node_counter = 0 - own_column = ["PID"] - parent_column = ["PPID"] + def next_id() -> str: + nonlocal node_counter + node_counter += 1 + return f"n{node_counter}" - if not((set(own_column).issubset(column_titles)) and (set(parent_column).issubset(column_titles))): - raise Exception("Plugin cannot be rendered as mermaid because there is no tree relationship.") - - tree_header = "graph TD\n" - branch_data = f"{tree_header}" + parent_stack: List[str] = [] + prev_depth = 0 + prev_id: Optional[str] = None - for (_depth, line) in final_output: - nums_line = max([len(line[column]) for column in line]) - for column in line: - line[column] = line[column] + ([""] * (nums_line - len(line[column]))) - for index in range(nums_line): - node_data = "" - for column in grid.columns: - node_data += f"{column.name}:{line[column][index]}
" - if(column.name in own_column): - own = line[column][index] - if(column.name in parent_column): - parent = line[column][index] - branch_data += f"\t{parent} --> {own}[{node_data}]\n".replace("(", "").replace(")", "") - outfd.write("{}\n".format(branch_data)) + lines: List[str] = ["graph TD"] + for depth, label in rows: + node_id = next_id() + if prev_id is not None: + if depth > prev_depth: + # Descended one or more levels. Push prev_id once per + # level so subsequent pops align even when the tree + # skips levels (e.g. depth 1 -> depth 3). + for _ in range(depth - prev_depth): + parent_stack.append(prev_id) + elif depth < prev_depth: + for _ in range(prev_depth - depth): + if parent_stack: + parent_stack.pop() + # depth == prev_depth: sibling, keep the same parent + + if parent_stack: + parent = parent_stack[-1] + lines.append(f'\t{parent} --> {node_id}["{label}"]') + else: + # Root-level node: declare it on its own. + lines.append(f'\t{node_id}["{label}"]') + + prev_id = node_id + prev_depth = depth + + outfd.write("\n".join(lines) + "\n") From 9207b242ec35440def0dbb28db9a0bf3aa88925b Mon Sep 17 00:00:00 2001 From: Donghyun Kim Date: Tue, 26 May 2026 08:46:57 +0900 Subject: [PATCH 7/8] Apply ruff format to MermaidRenderer Bring the new MermaidRenderer in line with the repository's ruff formatting rules (introduced in the develop merge that this branch just absorbed): double-quoted strings, trailing comma after the last mapping entry, four-space hanging indent for the visitor signature, and an additional blank line between top-level classes. --- volatility3/cli/text_renderer.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 1d5cf3f19..494606861 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -624,6 +624,7 @@ class JsonLinesRenderer(JsonRenderer): outfd.write(json.dumps(line, sort_keys=True)) outfd.write("\n") + class MermaidRenderer(CLIRenderer): _type_renderers = { format_hints.Bin: optional(lambda x: f"0b{x:b}"), @@ -633,7 +634,7 @@ class MermaidRenderer(CLIRenderer): interfaces.renderers.Disassembly: optional(display_disassembly), bytes: optional(lambda x: " ".join([f"{b:02x}" for b in x])), datetime.datetime: optional(lambda x: x.strftime("%Y-%m-%d %H:%M:%S.%f %Z")), - 'default': optional(lambda x: f"{x}") + "default": optional(lambda x: f"{x}"), } name = "mermaid" @@ -683,7 +684,7 @@ class MermaidRenderer(CLIRenderer): cells = [] for column_index, column in enumerate(grid.columns): renderer = self._type_renderers.get( - column.type, self._type_renderers['default'] + column.type, self._type_renderers["default"] ) value = renderer(node.values[column_index]) cells.append(f"{column.name}:{self._mermaid_label(value)}") @@ -692,8 +693,8 @@ class MermaidRenderer(CLIRenderer): rows: List[Tuple[int, str]] = [] def visitor( - node: interfaces.renderers.TreeNode, - accumulator: List[Tuple[int, str]], + node: interfaces.renderers.TreeNode, + accumulator: List[Tuple[int, str]], ) -> List[Tuple[int, str]]: accumulator.append((node.path_depth, format_row(node))) return accumulator From f66f5715c12ed1e4957a730e59310d760b9e8822 Mon Sep 17 00:00:00 2001 From: Donghyun Kim Date: Tue, 26 May 2026 16:33:41 +0900 Subject: [PATCH 8/8] Use itertools.count for MermaidRenderer node IDs Per review feedback, the small next_id() closure that combined a 'nonlocal node_counter' assignment with an f-string formatter is more naturally expressed as itertools.count. The counter generator yields the integer sequence starting at 1 and the call site formats it as 'n' on the spot, so the behaviour is unchanged: per-render, strictly-increasing, plugin-agnostic Mermaid node identifiers. --- volatility3/cli/text_renderer.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 494606861..8c4a27024 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -3,6 +3,7 @@ # import csv import datetime +import itertools import json import logging import random @@ -708,12 +709,7 @@ class MermaidRenderer(CLIRenderer): # PID) because (a) PID is not guaranteed unique across a TreeGrid, # (b) it is plugin-specific, and (c) Mermaid IDs must avoid # characters like parentheses that may appear in column data. - node_counter = 0 - - def next_id() -> str: - nonlocal node_counter - node_counter += 1 - return f"n{node_counter}" + node_ids = itertools.count(1) parent_stack: List[str] = [] prev_depth = 0 @@ -721,7 +717,7 @@ class MermaidRenderer(CLIRenderer): lines: List[str] = ["graph TD"] for depth, label in rows: - node_id = next_id() + node_id = f"n{next(node_ids)}" if prev_id is not None: if depth > prev_depth: # Descended one or more levels. Push prev_id once per