From 3392f02ade25f0fd6d2a3b4eb5de31de7369c075 Mon Sep 17 00:00:00 2001 From: rihi Date: Wed, 29 Nov 2023 14:27:50 +0000 Subject: [PATCH 01/14] Create draft PR for #369 From dad2453e3c7db8344272f7d86c097332904b3b38 Mon Sep 17 00:00:00 2001 From: rihi <19492038+rihi@users.noreply.github.com> Date: Wed, 29 Nov 2023 15:28:25 +0100 Subject: [PATCH 02/14] Fix export_plot on windows --- decompiler/util/decoration.py | 21 +++++++++++++++------ decompiler/util/to_dot_converter.py | 5 ++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/decompiler/util/decoration.py b/decompiler/util/decoration.py index 2453a4bcc..3626dad39 100644 --- a/decompiler/util/decoration.py +++ b/decompiler/util/decoration.py @@ -2,10 +2,11 @@ from __future__ import annotations import os +import subprocess import textwrap from logging import warning from re import compile -from subprocess import CompletedProcess, run +from subprocess import CompletedProcess, Popen, run from sys import stdout from tempfile import NamedTemporaryFile from typing import Dict, Optional, TextIO @@ -63,7 +64,7 @@ def _write_dot(self, handle: Optional[TextIO] = None): """Write the graph to the given handle or NamedTemporaryFile.""" if not handle: handle = NamedTemporaryFile(mode="w+") - ToDotConverter.write(self._graph, handle) + handle.write(ToDotConverter.write(self._graph)) handle.flush() handle.seek(0) return handle @@ -88,10 +89,18 @@ def export_plot(self, path: str, type="png"): path -- Path to the plot to be created. type -- a string describing the output type (commonly pdf, png) """ - with self._write_dot() as handle: - result = run(["dot", f"-T{type}", f"-o{path}", f"{handle.name}"], capture_output=True) - if result.returncode: - raise ValueError(f"Could not plot graph! ({result.stderr.decode('utf-8')}") + with Popen( + ["dot", f"-T{type}", f"-o{path}"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) as proc: + dot_source: str = ToDotConverter.write(self.graph) + stdout, stderr = proc.communicate(input=dot_source) + + if proc.returncode: + raise ValueError(f"Could not plot graph! ({stderr})") class DecoratedCFG(DecoratedGraph): diff --git a/decompiler/util/to_dot_converter.py b/decompiler/util/to_dot_converter.py index af169e573..b1d459d71 100644 --- a/decompiler/util/to_dot_converter.py +++ b/decompiler/util/to_dot_converter.py @@ -1,5 +1,4 @@ """Module handling conversion to dot-format.""" -from typing import TextIO from networkx import DiGraph @@ -16,10 +15,10 @@ def __init__(self, graph: DiGraph): self._graph = graph @classmethod - def write(cls, graph: DiGraph, handle: TextIO): + def write(cls, graph: DiGraph) -> str: """Write dot-format of given graph into handle.""" converter = cls(graph) - handle.write(converter._create_dot()) + return converter._create_dot() def _create_dot(self) -> str: """Create dot-file content.""" From 9643850ce6d97ce3b2b1db8124959ae30081983e Mon Sep 17 00:00:00 2001 From: rihi <19492038+rihi@users.noreply.github.com> Date: Wed, 29 Nov 2023 15:40:48 +0100 Subject: [PATCH 03/14] black --- decompiler/util/decoration.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/decompiler/util/decoration.py b/decompiler/util/decoration.py index 3626dad39..b7c4e3942 100644 --- a/decompiler/util/decoration.py +++ b/decompiler/util/decoration.py @@ -90,11 +90,7 @@ def export_plot(self, path: str, type="png"): type -- a string describing the output type (commonly pdf, png) """ with Popen( - ["dot", f"-T{type}", f"-o{path}"], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True + ["dot", f"-T{type}", f"-o{path}"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) as proc: dot_source: str = ToDotConverter.write(self.graph) stdout, stderr = proc.communicate(input=dot_source) From 68bdea600bb41303bead8cbca7cee41dfadfbaf5 Mon Sep 17 00:00:00 2001 From: rihi <19492038+rihi@users.noreply.github.com> Date: Thu, 30 Nov 2023 13:03:01 +0100 Subject: [PATCH 04/14] Move temporary file creation out of method --- decompiler/util/decoration.py | 14 +++++--------- tests/util/test_decoration.py | 10 +++++++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/decompiler/util/decoration.py b/decompiler/util/decoration.py index b7c4e3942..fd72b1eb2 100644 --- a/decompiler/util/decoration.py +++ b/decompiler/util/decoration.py @@ -9,7 +9,7 @@ from subprocess import CompletedProcess, Popen, run from sys import stdout from tempfile import NamedTemporaryFile -from typing import Dict, Optional, TextIO +from typing import Dict, TextIO import z3 from binaryninja import BranchType, EdgePenStyle, EdgeStyle, FlowGraph, FlowGraphNode, HighlightStandardColor, ThemeColor, show_graph_report @@ -60,20 +60,16 @@ def graph(self) -> DiGraph: """Return the graph being decorated.""" return self._graph - def _write_dot(self, handle: Optional[TextIO] = None): - """Write the graph to the given handle or NamedTemporaryFile.""" - if not handle: - handle = NamedTemporaryFile(mode="w+") + def _write_dot(self, handle: TextIO): + """Write the graph to the given handle.""" handle.write(ToDotConverter.write(self._graph)) - handle.flush() - handle.seek(0) - return handle def export_ascii(self) -> str: """Export the current graph into an ascii representation.""" if not GRAPH_EASY_INSTALLED: warning(f"Invoking graph-easy although it seems like it is not installed on the system.") - with self._write_dot() as handle: + with NamedTemporaryFile(mode="w+") as handle: + self._write_dot(handle) result: CompletedProcess = run(["graph-easy", "--as=ascii", handle.name], capture_output=True) return result.stdout.decode("utf-8") diff --git a/tests/util/test_decoration.py b/tests/util/test_decoration.py index 152d1fcb6..6c1fc03dd 100644 --- a/tests/util/test_decoration.py +++ b/tests/util/test_decoration.py @@ -1,3 +1,5 @@ +from io import StringIO + import pytest from binaryninja import HighlightStandardColor from decompiler.structures.ast.ast_nodes import SeqNode @@ -442,9 +444,11 @@ def test_ascii_switch(self, ast_switch): @pytest.mark.usefixtures("ast_condition") def test_dotviz_output(self, ast_condition): decorated = DecoratedAST.from_ast(ast_condition) - handle = decorated._write_dot() - data = handle.read() - handle.close() + + with StringIO() as stringIo: + decorated._write_dot(stringIo) + data = stringIo.getvalue() + assert all( [ x in data From 94862db371666a8caeb5e98b949bf2ac01ce3056 Mon Sep 17 00:00:00 2001 From: rihi <19492038+rihi@users.noreply.github.com> Date: Thu, 30 Nov 2023 13:12:17 +0100 Subject: [PATCH 05/14] Add missed flush --- decompiler/util/decoration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/decompiler/util/decoration.py b/decompiler/util/decoration.py index fd72b1eb2..b3b9cd622 100644 --- a/decompiler/util/decoration.py +++ b/decompiler/util/decoration.py @@ -70,6 +70,7 @@ def export_ascii(self) -> str: warning(f"Invoking graph-easy although it seems like it is not installed on the system.") with NamedTemporaryFile(mode="w+") as handle: self._write_dot(handle) + handle.flush() result: CompletedProcess = run(["graph-easy", "--as=ascii", handle.name], capture_output=True) return result.stdout.decode("utf-8") From 5bf9fe2137b9545dbd329260ca13bfb474452f28 Mon Sep 17 00:00:00 2001 From: Manuel Blatt Date: Thu, 30 Nov 2023 13:28:26 +0100 Subject: [PATCH 06/14] add tests for png export --- tests/util/test_decoration.py | 38 +++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/util/test_decoration.py b/tests/util/test_decoration.py index 6c1fc03dd..e055eeeb3 100644 --- a/tests/util/test_decoration.py +++ b/tests/util/test_decoration.py @@ -1,3 +1,4 @@ +import os from io import StringIO import pytest @@ -216,6 +217,22 @@ def test_convert_to_dot_with_string(self, graph_with_string): }""" ) + def test_png_export(self, simple_graph, png_magic): + decorated = DecoratedCFG.from_cfg(simple_graph) + export_path = "_test_cfg.png" + # remove potential left over file + try: + os.remove(export_path) + except OSError: + pass + # export plot + decorated.export_plot(export_path) + # check if exported plot has correct value (and implicitly if it exists) + with open(export_path, "rb") as file: + assert file.read(len(png_magic)) == png_magic + # remove file + os.remove(export_path) + class TestDecoratedAST: @pytest.fixture @@ -612,6 +629,22 @@ def test_convert_to_dot_switch(self, ast_switch): }""" ) + def test_png_export(self, ast_for_loop, png_magic): + decorated = DecoratedAST.from_ast(ast_for_loop) + export_path = "_test_ast.png" + # remove potential left over file + try: + os.remove(export_path) + except OSError: + pass + # export plot + decorated.export_plot(export_path) + # check if exported plot has correct value (and implicitly if it exists) + with open(export_path, "rb") as file: + assert file.read(len(png_magic)) == png_magic + # remove file + os.remove(export_path) + class TestDecoratedCode: @pytest.fixture @@ -644,3 +677,8 @@ def test_does_html_output(self, simple_code): html_code = decorated.export_html() assert len(html_code) > 100 assert r"\*" not in html_code + + +@pytest.fixture +def png_magic(): + return bytes.fromhex("89 50 4e 47 0d 0a 1a 0a") From ef8ccf5a35c571bcdd8e2d933bcba19bfe23cb63 Mon Sep 17 00:00:00 2001 From: Manuel Blatt Date: Thu, 30 Nov 2023 14:54:49 +0100 Subject: [PATCH 07/14] add graphviz to dockerfile requirements --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 8820baf40..38f4b455d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,7 @@ RUN apt -y update && apt -y upgrade && apt install -y --no-install-recommends \ virtualenv \ unzip \ astyle \ + graphviz \ # plotting ascii graphs for debug purposes libgraph-easy-perl \ z3 From 9157432ed4a2bbe93c8dabea61302fdd852765f6 Mon Sep 17 00:00:00 2001 From: rihi <19492038+rihi@users.noreply.github.com> Date: Wed, 6 Dec 2023 13:14:48 +0100 Subject: [PATCH 08/14] Use CloseableNamedTemporaryFile.py --- .../util/CloseableNamedTemporaryFile.py | 19 ++++++ decompiler/util/decoration.py | 68 ++++++++----------- 2 files changed, 49 insertions(+), 38 deletions(-) create mode 100644 decompiler/util/CloseableNamedTemporaryFile.py diff --git a/decompiler/util/CloseableNamedTemporaryFile.py b/decompiler/util/CloseableNamedTemporaryFile.py new file mode 100644 index 000000000..1a1bd0a20 --- /dev/null +++ b/decompiler/util/CloseableNamedTemporaryFile.py @@ -0,0 +1,19 @@ +from contextlib import contextmanager +from tempfile import NamedTemporaryFile + + +@contextmanager +def closeable_temporary_file(**kwargs): + """ + Context manager wrapper for NamedTemporaryFile, which allows + closing the file handle without deleting the underling file. + Deletion is delegated to the context manager closing. + + Note: With Python 3.12, a new parameter 'delete_on_close' is introduced + for NamedTemporaryFile which accomplishes the same thing. Consequently, + this api should be replaced when the code is updated to 3.12. + """ + + kwargs["delete"] = False + with NamedTemporaryFile(**kwargs) as file: + yield file diff --git a/decompiler/util/decoration.py b/decompiler/util/decoration.py index b3b9cd622..53a332ee1 100644 --- a/decompiler/util/decoration.py +++ b/decompiler/util/decoration.py @@ -1,14 +1,11 @@ """Module handling plotting and pretty printing.""" from __future__ import annotations -import os -import subprocess import textwrap from logging import warning from re import compile -from subprocess import CompletedProcess, Popen, run +from subprocess import CompletedProcess, run from sys import stdout -from tempfile import NamedTemporaryFile from typing import Dict, TextIO import z3 @@ -31,6 +28,7 @@ from decompiler.structures.ast.syntaxtree import AbstractSyntaxTree from decompiler.structures.graphs.cfg import BasicBlock, BasicBlockEdge, BasicBlockEdgeCondition, ControlFlowGraph from decompiler.structures.pseudo.operations import Condition +from decompiler.util.CloseableNamedTemporaryFile import closeable_temporary_file from decompiler.util.to_dot_converter import ToDotConverter from networkx import DiGraph from pygments import format, lex @@ -68,11 +66,13 @@ def export_ascii(self) -> str: """Export the current graph into an ascii representation.""" if not GRAPH_EASY_INSTALLED: warning(f"Invoking graph-easy although it seems like it is not installed on the system.") - with NamedTemporaryFile(mode="w+") as handle: - self._write_dot(handle) - handle.flush() - result: CompletedProcess = run(["graph-easy", "--as=ascii", handle.name], capture_output=True) - return result.stdout.decode("utf-8") + + with closeable_temporary_file(mode="w", encoding="utf-8") as file: + self._write_dot(file) + file.close() + + result: CompletedProcess = run(["graph-easy", "--as=ascii", file.name], capture_output=True) + return result.stdout.decode("utf-8") def export_dot(self, path: str): """Export the graph into a dotfile at the given location.""" @@ -86,14 +86,14 @@ def export_plot(self, path: str, type="png"): path -- Path to the plot to be created. type -- a string describing the output type (commonly pdf, png) """ - with Popen( - ["dot", f"-T{type}", f"-o{path}"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True - ) as proc: - dot_source: str = ToDotConverter.write(self.graph) - stdout, stderr = proc.communicate(input=dot_source) - if proc.returncode: - raise ValueError(f"Could not plot graph! ({stderr})") + with closeable_temporary_file(mode="w", encoding="utf-8") as file: + self._write_dot(file) + file.close() + + result = run(["dot", f"-T{type}", f"-o{path}", f"{file.name}"], capture_output=True) + if result.returncode: + raise ValueError(f"Could not plot graph! ({result.stderr.decode('utf-8')}") class DecoratedCFG(DecoratedGraph): @@ -303,22 +303,6 @@ def _format_node_content(label: str, max_width: int = 60): class DecoratedCode: """Class representing C code ready for pretty printing.""" - class TempFile: - """Context manager to write content to NamedTemporaryFile and release for windows, returns file name""" - - def __init__(self, content: str): - self.tmpf = NamedTemporaryFile(mode="w", delete=False) - self.tmpf.write(content) - self.name = self.tmpf.name - self.tmpf.flush() - self.tmpf.close() - - def __enter__(self) -> str: - return self.name - - def __exit__(self, exc_type, exc_val, exc_tb): - os.unlink(self.name) - def __init__(self, code: str, style="paraiso-dark"): """Generate an object handling code decoration.""" self._text = code @@ -360,15 +344,23 @@ def reformat(self): """Call astyle on command line to reformat the code.""" if not ASTYLE_INSTALLED: warning(f"Invoking astyle although it seems like it is not installed on the system.") - with self.TempFile(self._text) as filename: - run(["astyle", "-z2", "-n", filename], check=True, capture_output=True) - with open(filename, "r") as output: + + with closeable_temporary_file(mode="w", encoding="utf-8") as file: + file.write(self._text) + file.close() + + run(["astyle", "-z2", "-n", file.name], check=True, capture_output=True) + + with open(file.name, "r") as output: self._text = output.read() def export_ascii(self) -> str: - with self.TempFile(self._text) as filename: - result: CompletedProcess = run(["pygmentize", "-l", "cpp", f"-O style={self._style}", filename], capture_output=True) - return result.stdout.decode("ascii") + with closeable_temporary_file(mode="w", encoding="utf-8") as file: + file.write(self._text) + file.close() + + result: CompletedProcess = run(["pygmentize", "-l", "cpp", f"-O style={self._style}", file.name], capture_output=True) + return result.stdout.decode("ascii") def export_html(self) -> str: """Export an html representation of the current code.""" From 2cdc4a751281b41d8c05fb4a64d4e2af80eba338 Mon Sep 17 00:00:00 2001 From: rihi <19492038+rihi@users.noreply.github.com> Date: Wed, 6 Dec 2023 13:41:59 +0100 Subject: [PATCH 09/14] Remove unnecessary use of temporary file --- decompiler/util/decoration.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/decompiler/util/decoration.py b/decompiler/util/decoration.py index 53a332ee1..579e779c9 100644 --- a/decompiler/util/decoration.py +++ b/decompiler/util/decoration.py @@ -31,9 +31,10 @@ from decompiler.util.CloseableNamedTemporaryFile import closeable_temporary_file from decompiler.util.to_dot_converter import ToDotConverter from networkx import DiGraph -from pygments import format, lex +from pygments import highlight from pygments.formatters.html import HtmlFormatter -from pygments.lexers.c_like import CLexer +from pygments.formatters.terminal import TerminalFormatter +from pygments.lexers.c_cpp import CppLexer try: run(["graph-easy", "-v"], capture_output=True) @@ -355,17 +356,11 @@ def reformat(self): self._text = output.read() def export_ascii(self) -> str: - with closeable_temporary_file(mode="w", encoding="utf-8") as file: - file.write(self._text) - file.close() - - result: CompletedProcess = run(["pygmentize", "-l", "cpp", f"-O style={self._style}", file.name], capture_output=True) - return result.stdout.decode("ascii") + return highlight(self._text, CppLexer(), TerminalFormatter(style=self._style)) def export_html(self) -> str: """Export an html representation of the current code.""" - tokens = lex(self._text, CLexer()) - html = format(tokens, HtmlFormatter(full=True, style=self._style)) + html = highlight(self._text, CppLexer(), HtmlFormatter(full=True, style=self._style)) return self._filter_css_comments(html) @staticmethod From fdc6aaac83326c20634906bdad1851bbf8bb800b Mon Sep 17 00:00:00 2001 From: rihi <19492038+rihi@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:46:40 +0100 Subject: [PATCH 10/14] Add missing file deletion for 'closeable_temporary_file' --- decompiler/util/CloseableNamedTemporaryFile.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/decompiler/util/CloseableNamedTemporaryFile.py b/decompiler/util/CloseableNamedTemporaryFile.py index 1a1bd0a20..ffaead0e0 100644 --- a/decompiler/util/CloseableNamedTemporaryFile.py +++ b/decompiler/util/CloseableNamedTemporaryFile.py @@ -1,3 +1,4 @@ +import os from contextlib import contextmanager from tempfile import NamedTemporaryFile @@ -16,4 +17,7 @@ def closeable_temporary_file(**kwargs): kwargs["delete"] = False with NamedTemporaryFile(**kwargs) as file: - yield file + try: + yield file + finally: + os.remove(file.name) From 1ba16716305be625c78c66681aff18984f0747c5 Mon Sep 17 00:00:00 2001 From: rihi <19492038+rihi@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:49:43 +0100 Subject: [PATCH 11/14] Rename CloseableNamedTemporaryFile.py --- ...TemporaryFile.py => closeable_named_temporary_file.py} | 2 +- decompiler/util/decoration.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) rename decompiler/util/{CloseableNamedTemporaryFile.py => closeable_named_temporary_file.py} (94%) diff --git a/decompiler/util/CloseableNamedTemporaryFile.py b/decompiler/util/closeable_named_temporary_file.py similarity index 94% rename from decompiler/util/CloseableNamedTemporaryFile.py rename to decompiler/util/closeable_named_temporary_file.py index ffaead0e0..a8ac2f8cb 100644 --- a/decompiler/util/CloseableNamedTemporaryFile.py +++ b/decompiler/util/closeable_named_temporary_file.py @@ -4,7 +4,7 @@ @contextmanager -def closeable_temporary_file(**kwargs): +def CloseableNamedTemporaryFile(**kwargs): """ Context manager wrapper for NamedTemporaryFile, which allows closing the file handle without deleting the underling file. diff --git a/decompiler/util/decoration.py b/decompiler/util/decoration.py index 579e779c9..44a2aa281 100644 --- a/decompiler/util/decoration.py +++ b/decompiler/util/decoration.py @@ -28,7 +28,7 @@ from decompiler.structures.ast.syntaxtree import AbstractSyntaxTree from decompiler.structures.graphs.cfg import BasicBlock, BasicBlockEdge, BasicBlockEdgeCondition, ControlFlowGraph from decompiler.structures.pseudo.operations import Condition -from decompiler.util.CloseableNamedTemporaryFile import closeable_temporary_file +from decompiler.util.closeable_named_temporary_file import CloseableNamedTemporaryFile from decompiler.util.to_dot_converter import ToDotConverter from networkx import DiGraph from pygments import highlight @@ -68,7 +68,7 @@ def export_ascii(self) -> str: if not GRAPH_EASY_INSTALLED: warning(f"Invoking graph-easy although it seems like it is not installed on the system.") - with closeable_temporary_file(mode="w", encoding="utf-8") as file: + with CloseableNamedTemporaryFile(mode="w", encoding="utf-8") as file: self._write_dot(file) file.close() @@ -88,7 +88,7 @@ def export_plot(self, path: str, type="png"): type -- a string describing the output type (commonly pdf, png) """ - with closeable_temporary_file(mode="w", encoding="utf-8") as file: + with CloseableNamedTemporaryFile(mode="w", encoding="utf-8") as file: self._write_dot(file) file.close() @@ -346,7 +346,7 @@ def reformat(self): if not ASTYLE_INSTALLED: warning(f"Invoking astyle although it seems like it is not installed on the system.") - with closeable_temporary_file(mode="w", encoding="utf-8") as file: + with CloseableNamedTemporaryFile(mode="w", encoding="utf-8") as file: file.write(self._text) file.close() From f81eddf90643fa1e0586a9c7315664bd05020603 Mon Sep 17 00:00:00 2001 From: Manuel Blatt Date: Thu, 7 Dec 2023 12:57:26 +0100 Subject: [PATCH 12/14] test CloseableNamedTemporaryFile --- .../test_closeable_named_temporary_file.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tests/util/test_closeable_named_temporary_file.py diff --git a/tests/util/test_closeable_named_temporary_file.py b/tests/util/test_closeable_named_temporary_file.py new file mode 100644 index 000000000..e14eb6a6a --- /dev/null +++ b/tests/util/test_closeable_named_temporary_file.py @@ -0,0 +1,26 @@ +import os +from decompiler.util.closeable_named_temporary_file import CloseableNamedTemporaryFile + + +class TestCloseableNamedTemporaryFile: + def test_usage_after_closing(self): + with CloseableNamedTemporaryFile(mode = "w") as file: + file.write("test") + file.close() + with open(file.name, "r") as reopened_file: + assert reopened_file.read() == "test" + + def test_deletion_with_close(self): + with CloseableNamedTemporaryFile(mode = "w") as file: + file.close() + assert not os.path.exists(file.name) + + def test_deletion_without_close(self): + with CloseableNamedTemporaryFile(mode = "w") as file: + pass + assert not os.path.exists(file.name) + + def test_close_after_delete(self): + with CloseableNamedTemporaryFile(mode = "w") as file: + pass + file.close() From 68115ddda72537582a00333cffa058b9aa5f009e Mon Sep 17 00:00:00 2001 From: Manuel Blatt Date: Thu, 7 Dec 2023 12:57:47 +0100 Subject: [PATCH 13/14] close CloseableNamedTemporaryFile before removing --- decompiler/util/closeable_named_temporary_file.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/decompiler/util/closeable_named_temporary_file.py b/decompiler/util/closeable_named_temporary_file.py index a8ac2f8cb..90491e4ad 100644 --- a/decompiler/util/closeable_named_temporary_file.py +++ b/decompiler/util/closeable_named_temporary_file.py @@ -20,4 +20,10 @@ def CloseableNamedTemporaryFile(**kwargs): try: yield file finally: + # Close the file to be sure that it can be removed. + # It's ok if the file was already closed because NamedTemporaryFile's close method is idempotent. + file.close() + # If file was already deleted outside of this contextmanager, this will crash + # (just like the original NamedTemporaryFile). + # On NT, this might also crash if another handle to this file is still open os.remove(file.name) From fabb1ce32169d1510f8558aa1edbb983af660f71 Mon Sep 17 00:00:00 2001 From: Manuel Blatt Date: Thu, 7 Dec 2023 13:09:42 +0100 Subject: [PATCH 14/14] black --- tests/util/test_closeable_named_temporary_file.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/util/test_closeable_named_temporary_file.py b/tests/util/test_closeable_named_temporary_file.py index e14eb6a6a..3d1e7611a 100644 --- a/tests/util/test_closeable_named_temporary_file.py +++ b/tests/util/test_closeable_named_temporary_file.py @@ -1,26 +1,27 @@ import os + from decompiler.util.closeable_named_temporary_file import CloseableNamedTemporaryFile class TestCloseableNamedTemporaryFile: def test_usage_after_closing(self): - with CloseableNamedTemporaryFile(mode = "w") as file: + with CloseableNamedTemporaryFile(mode="w") as file: file.write("test") file.close() with open(file.name, "r") as reopened_file: assert reopened_file.read() == "test" def test_deletion_with_close(self): - with CloseableNamedTemporaryFile(mode = "w") as file: + with CloseableNamedTemporaryFile(mode="w") as file: file.close() assert not os.path.exists(file.name) def test_deletion_without_close(self): - with CloseableNamedTemporaryFile(mode = "w") as file: + with CloseableNamedTemporaryFile(mode="w") as file: pass assert not os.path.exists(file.name) def test_close_after_delete(self): - with CloseableNamedTemporaryFile(mode = "w") as file: + with CloseableNamedTemporaryFile(mode="w") as file: pass file.close()