declib 3.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. declib/__init__.py +9 -0
  2. declib/__main__.py +190 -0
  3. declib/api/__init__.py +13 -0
  4. declib/api/artifact_dict.py +153 -0
  5. declib/api/artifact_lifter.py +161 -0
  6. declib/api/decompiler_client.py +1219 -0
  7. declib/api/decompiler_interface.py +1261 -0
  8. declib/api/decompiler_server.py +782 -0
  9. declib/api/server_registry.py +171 -0
  10. declib/api/type_definition_parser.py +201 -0
  11. declib/api/type_parser.py +409 -0
  12. declib/api/utils.py +31 -0
  13. declib/artifacts/__init__.py +93 -0
  14. declib/artifacts/artifact.py +311 -0
  15. declib/artifacts/comment.py +49 -0
  16. declib/artifacts/context.py +61 -0
  17. declib/artifacts/decompilation.py +35 -0
  18. declib/artifacts/enum.py +53 -0
  19. declib/artifacts/formatting.py +27 -0
  20. declib/artifacts/func.py +433 -0
  21. declib/artifacts/global_variable.py +31 -0
  22. declib/artifacts/patch.py +49 -0
  23. declib/artifacts/segment.py +37 -0
  24. declib/artifacts/stack_variable.py +50 -0
  25. declib/artifacts/struct.py +184 -0
  26. declib/artifacts/typedef.py +59 -0
  27. declib/cli/__init__.py +3 -0
  28. declib/cli/decompiler_cli.py +1487 -0
  29. declib/configuration.py +184 -0
  30. declib/decompiler_stubs/__init__.py +0 -0
  31. declib/decompiler_stubs/angr_declib/__init__.py +4 -0
  32. declib/decompiler_stubs/binja_declib/__init__.py +4 -0
  33. declib/decompiler_stubs/ida_declib.py +8 -0
  34. declib/decompilers/__init__.py +8 -0
  35. declib/decompilers/angr/__init__.py +11 -0
  36. declib/decompilers/angr/artifact_lifter.py +46 -0
  37. declib/decompilers/angr/compat.py +262 -0
  38. declib/decompilers/angr/interface.py +949 -0
  39. declib/decompilers/binja/__init__.py +0 -0
  40. declib/decompilers/binja/artifact_lifter.py +32 -0
  41. declib/decompilers/binja/hooks.py +201 -0
  42. declib/decompilers/binja/interface.py +795 -0
  43. declib/decompilers/ghidra/__init__.py +0 -0
  44. declib/decompilers/ghidra/artifact_lifter.py +60 -0
  45. declib/decompilers/ghidra/compat/__init__.py +0 -0
  46. declib/decompilers/ghidra/compat/headless.py +156 -0
  47. declib/decompilers/ghidra/compat/imports.py +78 -0
  48. declib/decompilers/ghidra/compat/state.py +54 -0
  49. declib/decompilers/ghidra/compat/transaction.py +30 -0
  50. declib/decompilers/ghidra/hooks.py +242 -0
  51. declib/decompilers/ghidra/interface.py +1433 -0
  52. declib/decompilers/ida/__init__.py +0 -0
  53. declib/decompilers/ida/artifact_lifter.py +51 -0
  54. declib/decompilers/ida/compat.py +2054 -0
  55. declib/decompilers/ida/hooks.py +700 -0
  56. declib/decompilers/ida/ida_ui.py +80 -0
  57. declib/decompilers/ida/interface.py +659 -0
  58. declib/logger.py +101 -0
  59. declib/plugin_installer.py +259 -0
  60. declib/skills/__init__.py +24 -0
  61. declib/skills/decompiler/SKILL.md +316 -0
  62. declib/ui/__init__.py +33 -0
  63. declib/ui/qt_objects.py +146 -0
  64. declib/ui/utils.py +115 -0
  65. declib/ui/version.py +14 -0
  66. declib-3.8.0.dist-info/METADATA +138 -0
  67. declib-3.8.0.dist-info/RECORD +71 -0
  68. declib-3.8.0.dist-info/WHEEL +5 -0
  69. declib-3.8.0.dist-info/entry_points.txt +3 -0
  70. declib-3.8.0.dist-info/licenses/LICENSE +24 -0
  71. declib-3.8.0.dist-info/top_level.txt +1 -0
declib/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ __version__ = "3.8.0"
2
+
3
+
4
+ import logging
5
+ logging.getLogger("declib").addHandler(logging.NullHandler())
6
+ from declib.logger import Loggers
7
+ loggers = Loggers()
8
+ del Loggers
9
+ del logging
declib/__main__.py ADDED
@@ -0,0 +1,190 @@
1
+ import argparse
2
+ import sys
3
+ import logging
4
+
5
+ from declib.plugin_installer import DecLibPluginInstaller
6
+
7
+ _l = logging.getLogger(__name__)
8
+
9
+
10
+ def install():
11
+ DecLibPluginInstaller().install()
12
+
13
+
14
+ def start_server(
15
+ socket_path=None, decompiler=None, binary_path=None, headless=False,
16
+ server_id=None, project_dir=None,
17
+ ):
18
+ """Start the DecompilerServer (AF_UNIX socket-based)"""
19
+ try:
20
+ from declib.api.decompiler_server import DecompilerServer
21
+ from declib.api.decompiler_interface import DecompilerInterface
22
+
23
+ # Configure logging
24
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
25
+
26
+ # Prepare interface kwargs
27
+ interface_kwargs = {}
28
+ if decompiler:
29
+ interface_kwargs['force_decompiler'] = decompiler
30
+ if binary_path:
31
+ interface_kwargs['binary_path'] = binary_path
32
+ if headless:
33
+ interface_kwargs['headless'] = headless
34
+ if project_dir:
35
+ interface_kwargs['project_dir'] = project_dir
36
+
37
+ # Create and start server
38
+ if socket_path:
39
+ _l.info(f"Starting AF_UNIX DecompilerServer on {socket_path}")
40
+ else:
41
+ _l.info("Starting AF_UNIX DecompilerServer with auto-generated socket path")
42
+ if interface_kwargs:
43
+ _l.info(f"Interface options: {interface_kwargs}")
44
+
45
+ with DecompilerServer(socket_path=socket_path, server_id=server_id, **interface_kwargs) as server:
46
+ _l.info("Server started successfully. Press Ctrl+C to stop.")
47
+ _l.info("Connect with: DecompilerClient.discover('unix://{}')".format(server.socket_path))
48
+ try:
49
+ server.wait_for_shutdown()
50
+ except KeyboardInterrupt:
51
+ _l.info("Shutting down server...")
52
+
53
+ except ImportError as e:
54
+ _l.error(f"Failed to import required modules: {e}")
55
+ sys.exit(1)
56
+ except Exception as e:
57
+ _l.error(f"Failed to start server: {e}")
58
+ sys.exit(1)
59
+
60
+
61
+ def test_client(server_url=None):
62
+ """Test the DecompilerClient connection"""
63
+ try:
64
+ from declib.api.decompiler_client import DecompilerClient
65
+
66
+ # Configure logging
67
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
68
+
69
+ if server_url:
70
+ _l.info(f"Testing connection to DecompilerServer at {server_url}")
71
+ else:
72
+ _l.info("Testing connection to auto-discovered DecompilerServer")
73
+
74
+ with DecompilerClient.discover(server_url=server_url) as client:
75
+ _l.info(f"Successfully connected to {client.name} decompiler")
76
+ _l.info(f"Binary path: {client.binary_path}")
77
+ _l.info(f"Binary hash: {client.binary_hash}")
78
+ _l.info(f"Decompiler available: {client.decompiler_available}")
79
+
80
+ # Test fast artifact collections (benchmark performance)
81
+ import time
82
+ start_time = time.time()
83
+ functions = list(client.functions.items())
84
+ end_time = time.time()
85
+ _l.info(f"Retrieved {len(functions)} functions in {end_time - start_time:.3f}s")
86
+
87
+ start_time = time.time()
88
+ comments = list(client.comments.keys())
89
+ end_time = time.time()
90
+ _l.info(f"Retrieved {len(comments)} comment keys in {end_time - start_time:.3f}s")
91
+
92
+ _l.info("✅ Client test completed successfully!")
93
+
94
+ except ImportError as e:
95
+ _l.error(f"Failed to import required modules: {e}")
96
+ sys.exit(1)
97
+ except Exception as e:
98
+ _l.error(f"Client test failed: {e}")
99
+ sys.exit(1)
100
+
101
+
102
+ def main():
103
+ parser = argparse.ArgumentParser(
104
+ description="""
105
+ The DecLib Command Line Util. This is the script interface to DecLib that allows you to install and run
106
+ the Ghidra UI for running plugins, and start the DecompilerServer.
107
+ """,
108
+ epilog="""
109
+ Examples:
110
+ declib --install |
111
+ declib --server --socket-path /tmp/my_server.sock |
112
+ declib --server --decompiler ghidra --binary-path /path/to/binary --headless
113
+ """
114
+ )
115
+ parser.add_argument(
116
+ "--install", action="store_true", help="""
117
+ Install all the DecLib plugins to every decompiler.
118
+ """
119
+ )
120
+ parser.add_argument(
121
+ "--single-decompiler-install", nargs=2, metavar=('decompiler', 'path'), help="Install DAILA into a single decompiler. Decompiler must be one of: ida, ghidra, binja, angr."
122
+ )
123
+ parser.add_argument(
124
+ "--server", action="store_true", help="""
125
+ Start the DecompilerServer to expose DecompilerInterface APIs over AF_UNIX sockets.
126
+ """
127
+ )
128
+ parser.add_argument(
129
+ "--server-url", help="""
130
+ URL of the DecompilerServer to connect to (e.g., unix:///tmp/server.sock).
131
+ If not specified, will auto-discover running servers.
132
+ """
133
+ )
134
+ parser.add_argument(
135
+ "--socket-path", help="""
136
+ Path for the AF_UNIX socket (default: auto-generated in temp directory).
137
+ """
138
+ )
139
+ parser.add_argument(
140
+ "--decompiler", choices=["ida", "ghidra", "binja", "angr"], help="""
141
+ Force a specific decompiler for the server. If not specified, auto-detection will be used.
142
+ """
143
+ )
144
+ parser.add_argument(
145
+ "--binary-path", help="""
146
+ Path to the binary file to analyze (required for headless mode).
147
+ """
148
+ )
149
+ parser.add_argument(
150
+ "--headless", action="store_true", help="""
151
+ Run the decompiler in headless mode (no GUI). Requires --binary-path.
152
+ """
153
+ )
154
+ parser.add_argument(
155
+ "--server-id", help="""
156
+ Explicit server ID to use; if omitted, a unique one is generated.
157
+ """
158
+ )
159
+ parser.add_argument(
160
+ "--project-dir", help="""
161
+ Directory where the backend should store its project/database files
162
+ (Ghidra project, IDA .id*, etc.). If omitted, backend defaults apply
163
+ (Ghidra creates a project next to the binary; IDA writes .id* next
164
+ to the binary).
165
+ """
166
+ )
167
+ args = parser.parse_args()
168
+
169
+ if args.single_decompiler_install:
170
+ decompiler, path = args.single_decompiler_install
171
+ DecLibPluginInstaller().install(interactive=False, paths_by_target={decompiler: path})
172
+ elif args.install:
173
+ install()
174
+ elif args.server:
175
+ if args.headless and not args.binary_path:
176
+ parser.error("--headless requires --binary-path")
177
+ start_server(
178
+ socket_path=args.socket_path,
179
+ decompiler=args.decompiler,
180
+ binary_path=args.binary_path,
181
+ headless=args.headless,
182
+ server_id=args.server_id,
183
+ project_dir=args.project_dir,
184
+ )
185
+ else:
186
+ parser.print_help()
187
+
188
+
189
+ if __name__ == "__main__":
190
+ main()
declib/api/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ from .artifact_lifter import ArtifactLifter
2
+ from .decompiler_interface import DecompilerInterface
3
+ from .type_parser import CTypeParser, CType
4
+
5
+ from .decompiler_interface import (
6
+ DecompilerInterface
7
+ )
8
+ from .artifact_lifter import (
9
+ ArtifactLifter
10
+ )
11
+ from .type_parser import (
12
+ CTypeParser, CType
13
+ )
@@ -0,0 +1,153 @@
1
+ import typing
2
+ import logging
3
+
4
+ from declib.artifacts import (
5
+ Artifact, Comment, Enum, FunctionHeader, Function, FunctionArgument,
6
+ GlobalVariable, Patch, Segment, StackVariable, Struct, StructMember, Typedef
7
+ )
8
+
9
+ if typing.TYPE_CHECKING:
10
+ from declib.api import DecompilerInterface
11
+
12
+ _l = logging.getLogger(__name__)
13
+
14
+
15
+ class ArtifactDict(dict):
16
+ """
17
+ The ArtifactDict is a Dictionary wrapper around the getting/setting/listing of artifacts in the decompiler. This
18
+ allows for a more pythonic interface to the decompiler artifacts. For example, instead of doing:
19
+ deci._set_function(func)
20
+
21
+ You can do:
22
+ >>> deci.functions[func.addr] = func
23
+
24
+ This class is not meant to be instantiated directly, but rather through the DecompilerInterface class.
25
+ There is currently some interesting affects and caveats to using this class:
26
+ - When you list artifacts, by calling list(), you will get a light copy of the artifacts. This means that if you
27
+ modify the artifacts in the list, they will not be reflected in the decompiler. You also do need get current
28
+ data in the decompiler, only an acknowledgement that the artifact exists.
29
+ - You must reassign the artifact to the dictionary to update the decompiler.
30
+ - When assigning something to the dictionary, it must always be in its lifted form. You will also only get lifted
31
+ artifacts back from the dictionary.
32
+ - For convience, you can access functions by their lowered address
33
+ """
34
+
35
+ def __init__(self, artifact_cls, deci: "DecompilerInterface", error_on_duplicate=False, scopable=False):
36
+ super().__init__()
37
+
38
+ self._deci = deci
39
+ self._error_on_duplicate = error_on_duplicate
40
+ self._scopable = scopable
41
+ self._art_function = {
42
+ # ArtifactType: (setter, getter, lister)
43
+ Function: (self._deci._set_function, self._deci._get_function, self._deci._functions, self._deci._del_function),
44
+ StackVariable: (self._deci._set_stack_variable, self._deci._get_stack_variable, self._deci._stack_variables, self._deci._del_stack_variable),
45
+ GlobalVariable: (self._deci._set_global_variable, self._deci._get_global_var, self._deci._global_vars, self._deci._del_global_var),
46
+ Struct: (self._deci._set_struct, self._deci._get_struct, self._deci._structs, self._deci._del_struct),
47
+ Enum: (self._deci._set_enum, self._deci._get_enum, self._deci._enums, self._deci._del_enum),
48
+ Typedef: (self._deci._set_typedef, self._deci._get_typedef, self._deci._typedefs, self._deci._del_typedef),
49
+ Comment: (self._deci._set_comment, self._deci._get_comment, self._deci._comments, self._deci._del_comment),
50
+ Patch: (self._deci._set_patch, self._deci._get_patch, self._deci._patches, self._deci._del_patch),
51
+ Segment: (self._deci._set_segment, self._deci._get_segment, self._deci._segments, self._deci._del_segment)
52
+ }
53
+
54
+ functions = self._art_function.get(artifact_cls, None)
55
+ if functions is None:
56
+ raise ValueError(f"Attempting to create a dict for a Artifact class that is not supported: {artifact_cls}")
57
+
58
+ self._artifact_class = artifact_cls
59
+ self._artifact_setter, self._artifact_getter, self._artifact_lister, self._artifact_remover = functions
60
+
61
+ def __len__(self):
62
+ return len(self._artifact_lister())
63
+
64
+ def _lifted_art_lister(self):
65
+ d = self._artifact_lister()
66
+ d_items = list(d.items())
67
+ if not d_items:
68
+ return {}
69
+
70
+ is_addr = hasattr(d_items[0][1], "addr")
71
+ new_d = {}
72
+ for k, v in d_items:
73
+ if is_addr:
74
+ k = self._deci.art_lifter.lift_addr(k)
75
+ new_d[k] = self._deci.art_lifter.lift(v)
76
+
77
+ return new_d
78
+
79
+ def __getitem__(self, item):
80
+ """
81
+ Takes a lifted identifier as input and returns a lifted artifact
82
+ """
83
+ if isinstance(item, int):
84
+ item = self._deci.art_lifter.lower_addr(item)
85
+ if self._scopable and not self._deci.supports_type_scopes:
86
+ item, _ = self._deci.art_lifter.parse_scoped_type(item)
87
+
88
+ art = self._artifact_getter(item)
89
+ if art is None:
90
+ raise KeyError
91
+
92
+ return self._deci.art_lifter.lift(art)
93
+
94
+ def __setitem__(self, key, value):
95
+ """
96
+ Both key and value must be lifted artifacts
97
+ """
98
+ if not isinstance(value, self._artifact_class):
99
+ raise ValueError(f"Attempting to set a value of type {type(value)} to a dict of type {self._artifact_class}")
100
+
101
+ if isinstance(key, int):
102
+ key = self._deci.art_lifter.lower_addr(key)
103
+ if self._scopable and not self._deci.supports_type_scopes:
104
+ key, _ = self._deci.art_lifter.parse_scoped_type(key)
105
+
106
+ art = self._deci.art_lifter.lower(value)
107
+ if not self._artifact_setter(art) and self._error_on_duplicate:
108
+ raise ValueError(f"Set value {value} is already present at key {key}")
109
+
110
+ def __contains__(self, item):
111
+ if isinstance(item, int):
112
+ item = self._deci.art_lifter.lower_addr(item)
113
+ if self._scopable and not self._deci.supports_type_scopes:
114
+ item, _ = self._deci.art_lifter.parse_scoped_type(item)
115
+
116
+ data = self._artifact_getter(item)
117
+ return data is not None
118
+
119
+ def __delitem__(self, key):
120
+ if isinstance(key, int):
121
+ key = self._deci.art_lifter.lower_addr(key)
122
+ if self._scopable and not self._deci.supports_type_scopes:
123
+ key, _ = self._deci.art_lifter.parse_scoped_type(key)
124
+
125
+ art = self._artifact_getter(key)
126
+ if isinstance(art, Struct):
127
+ self._artifact_remover(key)
128
+ self._deci.struct_changed(art, deleted=True)
129
+ else:
130
+ self._artifact_remover(key)
131
+
132
+ def __iter__(self):
133
+ return iter(self._lifted_art_lister())
134
+
135
+ def __repr__(self):
136
+ return f"<{self.__class__.__name__}: {self._artifact_class.__name__} len={self.__len__()}>"
137
+
138
+ def __str__(self):
139
+ return f"{self._lifted_art_lister()}"
140
+
141
+ def keys(self):
142
+ return self._lifted_art_lister().keys()
143
+
144
+ def values(self):
145
+ return self._lifted_art_lister().values()
146
+
147
+ def items(self):
148
+ return self._lifted_art_lister().items()
149
+
150
+ def get(self, key, default=None):
151
+ if key in self:
152
+ return self[key]
153
+ return default
@@ -0,0 +1,161 @@
1
+ import logging
2
+ import typing
3
+
4
+ from declib.artifacts import StackVariable, Artifact, FunctionArgument, StructMember, Typedef, Enum, Struct
5
+ from declib.api.type_parser import CTypeParser
6
+
7
+ if typing.TYPE_CHECKING:
8
+ from declib.api import DecompilerInterface
9
+
10
+ _l = logging.getLogger(name=__name__)
11
+
12
+
13
+ class ArtifactLifter:
14
+ SCOPE_DELIMITER = "::"
15
+
16
+ def __init__(self, deci: "DecompilerInterface", types=None):
17
+ self.deci = deci
18
+ self.type_parser = CTypeParser(extra_types=types)
19
+
20
+ #
21
+ # Public API
22
+ #
23
+
24
+ def lift(self, artifact: Artifact):
25
+ return self._lift_or_lower_artifact(artifact, "lift")
26
+
27
+ def lower(self, artifact: Artifact):
28
+ return self._lift_or_lower_artifact(artifact, "lower")
29
+
30
+ #
31
+ # Special handlers for scopes
32
+ #
33
+
34
+ @staticmethod
35
+ def parse_scoped_type(type_str: str) -> tuple[str, str | None]:
36
+ """
37
+ Parses a scoped type string into its base type and scope.
38
+ Note: the scope can be None if the type is not scoped.
39
+
40
+ Examples:
41
+ 'stdint::uint32_t' -> ('uint32_t', 'stdint')
42
+ 'uint32_t' -> ('uint32_t', None)
43
+ """
44
+ if not type_str:
45
+ return "", None
46
+
47
+ # check if the type is scoped
48
+ scope = None
49
+ deli = ArtifactLifter.SCOPE_DELIMITER
50
+ if deli in type_str:
51
+ scope_parts = type_str.split(deli)
52
+ base_type = scope_parts[-1]
53
+ scope = deli.join(scope_parts[:-1])
54
+ else:
55
+ base_type = type_str
56
+
57
+ return base_type, scope
58
+
59
+ @staticmethod
60
+ def scoped_type_to_str(name: str, scope: str | None = None) -> str:
61
+ """
62
+ Converts a name and scope into a scoped type string.
63
+ Note: the scope can be None if the type is not scoped.
64
+ """
65
+ return name if not scope else f"{scope}::{name}"
66
+
67
+ #
68
+ # Override Mandatory Funcs
69
+ #
70
+
71
+ def lift_type(self, type_str: str) -> str:
72
+ return type_str
73
+
74
+ def lift_addr(self, addr: int) -> int:
75
+ base_addr = self.deci.binary_base_addr
76
+ if addr < base_addr:
77
+ self.deci.warning(f"Lifting an address that appears already lifted: {addr}...")
78
+
79
+ return addr - base_addr
80
+
81
+ def lift_stack_offset(self, offset: int, func_addr: int) -> int:
82
+ pass
83
+
84
+ def lower_type(self, type_str: str) -> str:
85
+ if self.SCOPE_DELIMITER in type_str and not self.deci.supports_type_scopes:
86
+ type_str, scope = self.scoped_type_to_str(type_str)
87
+
88
+ return type_str
89
+
90
+ def lower_addr(self, addr: int) -> int:
91
+ base_addr = self.deci.binary_base_addr
92
+ if addr >= base_addr != 0:
93
+ self.deci.warning(f"Lowering an address that appears already lowered: {addr}...")
94
+
95
+ return addr + base_addr
96
+
97
+ def lower_stack_offset(self, offset: int, func_addr: int) -> int:
98
+ pass
99
+
100
+ #
101
+ # Private
102
+ #
103
+
104
+ def _lift_or_lower_artifact(self, artifact, mode):
105
+ target_attrs = ("name", "type", "offset", "addr", "func_addr", "line_map")
106
+ if mode not in ("lower", "lift"):
107
+ return None
108
+
109
+ if not isinstance(artifact, Artifact):
110
+ return artifact
111
+ lifted_art = artifact.copy()
112
+ # correct simple properties in the artifact
113
+ for attr in target_attrs:
114
+ if hasattr(lifted_art, attr):
115
+ curr_val = getattr(lifted_art, attr)
116
+ if curr_val is None:
117
+ continue
118
+
119
+ # special handling for stack variables
120
+ if attr == "offset":
121
+ if not isinstance(artifact, StackVariable):
122
+ continue
123
+ lifting_func = getattr(self, f"{mode}_stack_offset")
124
+ setattr(lifted_art, attr, lifting_func(curr_val, lifted_art.addr))
125
+ # special handling for decompilation
126
+ elif attr == "line_map":
127
+ lifted_line_map = {}
128
+ lift_or_lower_func = self.lift_addr if mode == "lift" else self.lower_addr
129
+ for k, v in curr_val.items():
130
+ lifted_line_map[k] = {lift_or_lower_func(_v) for _v in v}
131
+
132
+ setattr(lifted_art, attr, lifted_line_map)
133
+ # special handling for types that have names
134
+ elif attr == "name":
135
+ if not isinstance(artifact, (Typedef, Enum, Struct)):
136
+ continue
137
+ lifted_type = self.lift_type(curr_val) if mode == "lift" else self.lower_type(curr_val)
138
+ setattr(lifted_art, attr, lifted_type)
139
+ else:
140
+ attr_func_name = attr if attr != "func_addr" else "addr"
141
+ lifting_func = getattr(self, f"{mode}_{attr_func_name}")
142
+ setattr(lifted_art, attr, lifting_func(curr_val))
143
+
144
+ # recursively correct nested artifacts
145
+ for attr in lifted_art.__slots__:
146
+ attr_val = getattr(lifted_art, attr)
147
+ if not attr_val:
148
+ continue
149
+
150
+ # nested function headers
151
+ if attr == "header":
152
+ setattr(lifted_art, attr, self._lift_or_lower_artifact(attr_val, mode))
153
+ # nested args, stack_vars, or struct_members
154
+ elif isinstance(attr_val, dict):
155
+ nested_arts = {}
156
+ for k, v in attr_val.items():
157
+ nested_art = self._lift_or_lower_artifact(v, mode)
158
+ nested_arts[nested_art.offset if isinstance(nested_art, (StackVariable, FunctionArgument, StructMember)) else k] = nested_art
159
+ setattr(lifted_art, attr, nested_arts)
160
+
161
+ return lifted_art