gdare2 0.1.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 (83) hide show
  1. gdare2/__init__.py +4 -0
  2. gdare2/__main__.py +3 -0
  3. gdare2/arch_matrix.py +33 -0
  4. gdare2/argv.py +417 -0
  5. gdare2/assembler.py +157 -0
  6. gdare2/cli.py +344 -0
  7. gdare2/client.py +112 -0
  8. gdare2/cmd/__init__.py +142 -0
  9. gdare2/cmd/analysis.py +367 -0
  10. gdare2/cmd/assemble.py +140 -0
  11. gdare2/cmd/comments.py +147 -0
  12. gdare2/cmd/eval.py +401 -0
  13. gdare2/cmd/extras.py +576 -0
  14. gdare2/cmd/flags.py +460 -0
  15. gdare2/cmd/info.py +481 -0
  16. gdare2/cmd/meta.py +228 -0
  17. gdare2/cmd/metadata.py +195 -0
  18. gdare2/cmd/misc.py +54 -0
  19. gdare2/cmd/print.py +915 -0
  20. gdare2/cmd/search.py +481 -0
  21. gdare2/cmd/seek.py +269 -0
  22. gdare2/cmd/types.py +355 -0
  23. gdare2/cmd/write.py +288 -0
  24. gdare2/cmd/xrefs.py +323 -0
  25. gdare2/completion.py +86 -0
  26. gdare2/config.py +197 -0
  27. gdare2/errors.py +103 -0
  28. gdare2/escape.py +31 -0
  29. gdare2/expr.py +262 -0
  30. gdare2/filter.py +243 -0
  31. gdare2/flags.py +96 -0
  32. gdare2/fmt/__init__.py +1 -0
  33. gdare2/fmt/codegen.py +143 -0
  34. gdare2/fmt/disasm.py +113 -0
  35. gdare2/fmt/graph.py +93 -0
  36. gdare2/fmt/listing.py +11 -0
  37. gdare2/fmt/memory.py +75 -0
  38. gdare2/fmt/strings.py +93 -0
  39. gdare2/formats/__init__.py +5 -0
  40. gdare2/formats/elf.py +888 -0
  41. gdare2/formats/magic.py +43 -0
  42. gdare2/jsonutil.py +22 -0
  43. gdare2/launcher.py +215 -0
  44. gdare2/parity.py +172 -0
  45. gdare2/parser.py +225 -0
  46. gdare2/pf.py +486 -0
  47. gdare2/pipe.py +59 -0
  48. gdare2/protocol.py +75 -0
  49. gdare2/r2pipe_server.py +177 -0
  50. gdare2/rc.py +74 -0
  51. gdare2/registry.py +217 -0
  52. gdare2/session.py +1087 -0
  53. gdare2-0.1.0.dist-info/METADATA +547 -0
  54. gdare2-0.1.0.dist-info/RECORD +83 -0
  55. gdare2-0.1.0.dist-info/WHEEL +4 -0
  56. gdare2-0.1.0.dist-info/entry_points.txt +3 -0
  57. gdare2-0.1.0.dist-info/licenses/LICENSE +21 -0
  58. gdare2d/__init__.py +3 -0
  59. gdare2d/__main__.py +245 -0
  60. gdare2d/context.py +29 -0
  61. gdare2d/dispatch.py +122 -0
  62. gdare2d/executor.py +68 -0
  63. gdare2d/import_plan.py +218 -0
  64. gdare2d/ops/__init__.py +1 -0
  65. gdare2d/ops/_helpers.py +88 -0
  66. gdare2d/ops/analysis.py +299 -0
  67. gdare2d/ops/assemble.py +70 -0
  68. gdare2d/ops/batch.py +220 -0
  69. gdare2d/ops/comments.py +151 -0
  70. gdare2d/ops/decompile.py +84 -0
  71. gdare2d/ops/disasm.py +221 -0
  72. gdare2d/ops/escape.py +26 -0
  73. gdare2d/ops/graph.py +163 -0
  74. gdare2d/ops/info.py +315 -0
  75. gdare2d/ops/memory.py +56 -0
  76. gdare2d/ops/names.py +125 -0
  77. gdare2d/ops/search.py +324 -0
  78. gdare2d/ops/session.py +74 -0
  79. gdare2d/ops/types.py +482 -0
  80. gdare2d/ops/write.py +246 -0
  81. gdare2d/ops/xrefs.py +61 -0
  82. gdare2d/runtime.py +551 -0
  83. gdare2d/server.py +146 -0
gdare2/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ """gdare2: a radare2-shaped client for Ghidra."""
2
+
3
+ __version__ = "0.1.0"
4
+ PROTOCOL_VERSION = 1
gdare2/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from gdare2.cli import main
2
+
3
+ raise SystemExit(main())
gdare2/arch_matrix.py ADDED
@@ -0,0 +1,33 @@
1
+ """Raw-binary architecture configurations used by the arch matrix."""
2
+
3
+ from __future__ import annotations
4
+
5
+ # Keep this list in sync with gdare2d.import_plan.resolve_alias.
6
+ ARCH_MATRIX = (
7
+ {"name": "x86-16", "arch": "x86", "bits": "16", "endian": "little", "language": "x86:LE:16:Real Mode"},
8
+ {"name": "x86-32", "arch": "x86", "bits": "32", "endian": "little", "language": "x86:LE:32:default"},
9
+ {"name": "x86-64", "arch": "x86", "bits": "64", "endian": "little", "language": "x86:LE:64:default"},
10
+ {"name": "arm-le", "arch": "armle", "bits": "32", "endian": "little", "language": "ARM:LE:32:v7"},
11
+ {"name": "arm-be", "arch": "armbe", "bits": "32", "endian": "big", "language": "ARM:BE:32:v7"},
12
+ {"name": "aarch64-le", "arch": "aarch64", "bits": "64", "endian": "little", "language": "AARCH64:LE:64:v8A"},
13
+ {"name": "aarch64-be", "arch": "aarch64be", "bits": "64", "endian": "big", "language": "AARCH64:BE:64:v8A"},
14
+ {"name": "mips-le", "arch": "mipsle", "bits": "32", "endian": "little", "language": "MIPS:LE:32:default"},
15
+ {"name": "mips-be", "arch": "mipsbe", "bits": "32", "endian": "big", "language": "MIPS:BE:32:default"},
16
+ {"name": "mips64-le", "arch": "mips64le", "bits": "64", "endian": "little", "language": "MIPS:LE:64:default"},
17
+ {"name": "mips64-be", "arch": "mips64be", "bits": "64", "endian": "big", "language": "MIPS:BE:64:default"},
18
+ {"name": "ppc-be", "arch": "ppc", "bits": "32", "endian": "big", "language": "PowerPC:BE:32:default"},
19
+ {"name": "ppc-le", "arch": "ppcle", "bits": "32", "endian": "little", "language": "PowerPC:LE:32:default"},
20
+ {"name": "ppc64-be", "arch": "ppc64", "bits": "64", "endian": "big", "language": "PowerPC:BE:64:default"},
21
+ {"name": "ppc64-le", "arch": "ppc64le", "bits": "64", "endian": "little", "language": "PowerPC:LE:64:default"},
22
+ {"name": "riscv32", "arch": "riscv32", "bits": "32", "endian": "little", "language": "RISCV:LE:32:default", "bytes": "13000000"},
23
+ {"name": "riscv64", "arch": "riscv64", "bits": "64", "endian": "little", "language": "RISCV:LE:64:default", "bytes": "13000000"},
24
+ {"name": "m68k", "arch": "m68k", "bits": "32", "endian": "big", "language": "68000:BE:32:default"},
25
+ {"name": "sparc", "arch": "sparc", "bits": "32", "endian": "big", "language": "sparc:BE:32:default"},
26
+ {"name": "sparc64", "arch": "sparc64", "bits": "64", "endian": "big", "language": "sparc:BE:64:default"},
27
+ {"name": "sh2a", "arch": "sh2a", "bits": "32", "endian": "big", "language": "SuperH:BE:32:SH-2A"},
28
+ {"name": "sh2", "arch": "sh2", "bits": "32", "endian": "big", "language": "SuperH:BE:32:SH-2"},
29
+ )
30
+
31
+ # Default decode probe: X86 NOPs plus RET. Entries may override it when the
32
+ # pattern is not a valid instruction for that architecture.
33
+ RAW_BYTES = bytes.fromhex("90909090c3")
gdare2/argv.py ADDED
@@ -0,0 +1,417 @@
1
+ """Hand-rolled radare2-style argument parser."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ from gdare2.errors import UsageError
8
+
9
+ VALUE_FLAGS = frozenset("abcBeIikmpst")
10
+ BOOLEAN_FLAGS = frozenset("0A1E2fhjLnNqQSTuvVw")
11
+ DOUBLED = ("AA", "NN", "qq", "LL", "nn")
12
+ IGNORED = {
13
+ "0": "output is never NUL-separated",
14
+ "1": "use the shell's 2>&1 instead",
15
+ "2": "use the shell's 2>&1 instead",
16
+ "E": "edit the rc file directly",
17
+ "f": "use `b $s` to set the block size",
18
+ "k": "gdare2 does not do syscall resolution",
19
+ "M": "use -e asm.demangle=false",
20
+ "nn": "-n already covers this",
21
+ "Q": "teardown saving is negligible",
22
+ "S": "use -e cfg.sandbox=true",
23
+ "t": "Ghidra analyses the project itself",
24
+ "T": "Ghidra has no IDA-style hash cache",
25
+ "u": "gdare2 does not filter symbol names",
26
+ }
27
+ # Ignored short options that still take a value in radare2.
28
+ IGNORED_VALUE = frozenset("k")
29
+
30
+ REJECTED = {
31
+ "C": "remote HTTP relay (-c+=...) is not implemented",
32
+ "d": "the debugger is not implemented",
33
+ "D": "the debugger is not implemented",
34
+ "F": "bin plugins are chosen by Ghidra's loader",
35
+ "H": "use -v or ?V for build information",
36
+ "r": "there is no debugger to profile",
37
+ "R": "there is no debugger to profile",
38
+ "x": "load permissions come from Ghidra's loader",
39
+ "X": "extraction plugins are not implemented",
40
+ "P": "rapatch would write with different semantics",
41
+ "o": "file-offset mapping is not implemented; use --list-offset for list pagination",
42
+ "z": "string loading is decided by Ghidra's analyser; use iz/izz",
43
+ }
44
+
45
+ LONG_VALUE = frozenset(
46
+ {
47
+ "attach",
48
+ "status",
49
+ "kill",
50
+ "project",
51
+ "project-name",
52
+ "program",
53
+ "ghidra-language",
54
+ "compiler",
55
+ "loader",
56
+ "linger",
57
+ "idle",
58
+ "timeout",
59
+ "startup-timeout",
60
+ "analysis-timeout",
61
+ "assembler",
62
+ "listen",
63
+ "color",
64
+ "endian",
65
+ "list-offset",
66
+ "pipe-listen",
67
+ }
68
+ )
69
+ LONG_BOOL = frozenset(
70
+ {
71
+ "help",
72
+ "version",
73
+ "no-spawn",
74
+ "save-on-close",
75
+ "no-save-on-close",
76
+ "sessions",
77
+ "json",
78
+ "force-language",
79
+ "force-base",
80
+ "wait",
81
+ "ephemeral",
82
+ "reimport",
83
+ "allow-exec",
84
+ "daemon-readonly",
85
+ }
86
+ )
87
+
88
+
89
+ @dataclass
90
+ class Options:
91
+ file: str | None = None
92
+ commands: list[str] = field(default_factory=list)
93
+ evals: list[tuple[str, str]] = field(default_factory=list)
94
+ pre_scripts: list[str] = field(default_factory=list)
95
+ post_scripts: list[str] = field(default_factory=list)
96
+ ignored: list[tuple[str, str]] = field(default_factory=list)
97
+
98
+ analyze: int = 0
99
+ quiet: int = 0
100
+ json: bool = False
101
+ readonly: bool = True
102
+ skip_rc: int = 0
103
+ color: int | None = None
104
+ seek: str | None = None
105
+
106
+ arch: str | None = None
107
+ bits: str | None = None
108
+ base: str | None = None
109
+ endian: str | None = None
110
+ no_analysis: bool = False
111
+ project: str | None = None
112
+ project_name: str | None = None
113
+ program: str | None = None
114
+ ghidra_language: str | None = None
115
+ compiler: str | None = None
116
+ loader: str | None = None
117
+ loader_args: list[tuple[str, str]] = field(default_factory=list)
118
+ force_language: bool = False
119
+ force_base: bool = False
120
+ reimport: bool = False
121
+
122
+ attach: str | None = None
123
+ status: str | None = None
124
+ kill: str | None = None
125
+ spawn: bool = True
126
+ linger: float | None = None
127
+ idle: float | None = None
128
+ timeout: float | None = None
129
+ startup_timeout: float | None = None
130
+ analysis_timeout: float | None = None
131
+ list_offset: int | None = None
132
+ pipe_listen: str | None = None
133
+ save_on_close: bool | None = None
134
+ wait: bool = False
135
+ assembler: str | None = None
136
+ daemon_readonly: bool = False
137
+ ephemeral: bool = False
138
+ allow_exec: bool = False
139
+ listen: str | None = None
140
+
141
+ show_help: bool = False
142
+ show_version: int = 0
143
+ list_sessions: bool = False
144
+
145
+ @property
146
+ def wants_database(self) -> bool:
147
+ return not (
148
+ self.show_help
149
+ or self.show_version
150
+ or self.list_sessions
151
+ or self.kill
152
+ or self.status
153
+ )
154
+
155
+
156
+ def parse(argv: list[str]) -> Options:
157
+ options = Options()
158
+ index = 0
159
+ only_positional = False
160
+ while index < len(argv):
161
+ token = argv[index]
162
+ index += 1
163
+ if only_positional or not token.startswith("-") or token == "-":
164
+ _set_file(options, token)
165
+ continue
166
+ if token == "--":
167
+ only_positional = True
168
+ continue
169
+ if token.startswith("--"):
170
+ index = _long(options, token[2:], argv, index)
171
+ continue
172
+ index = _short(options, token[1:], argv, index)
173
+ return options
174
+
175
+
176
+ def _set_file(options: Options, token: str) -> None:
177
+ if options.file is not None:
178
+ raise UsageError(f"only one file can be opened, got {options.file!r} and {token!r}")
179
+ options.file = token
180
+
181
+
182
+ def _long(options: Options, body: str, argv: list[str], index: int) -> int:
183
+ name, _, inline = body.partition("=")
184
+ if name == "loader-arg":
185
+ key = inline or _next(argv, index, "--loader-arg key")
186
+ index += 0 if inline else 1
187
+ value = _next(argv, index, "--loader-arg value")
188
+ index += 1
189
+ options.loader_args.append((key, value))
190
+ return index
191
+ if name in LONG_VALUE:
192
+ value = inline or _next(argv, index, f"--{name}")
193
+ index += 0 if inline else 1
194
+ _apply_long_value(options, name, value)
195
+ return index
196
+ if name in LONG_BOOL:
197
+ if inline:
198
+ raise UsageError(f"--{name} does not take a value")
199
+ _apply_long_bool(options, name)
200
+ return index
201
+ raise UsageError(f"unknown option --{name}")
202
+
203
+
204
+ def _apply_long_value(options: Options, name: str, value: str) -> None:
205
+ if name == "attach":
206
+ options.attach = value
207
+ elif name == "status":
208
+ options.status = value
209
+ elif name == "kill":
210
+ options.kill = value
211
+ elif name == "project":
212
+ options.project = value
213
+ elif name == "project-name":
214
+ options.project_name = value
215
+ elif name == "program":
216
+ options.program = value
217
+ elif name == "ghidra-language":
218
+ options.ghidra_language = value
219
+ elif name == "compiler":
220
+ options.compiler = value
221
+ elif name == "loader":
222
+ options.loader = value
223
+ elif name == "linger":
224
+ options.linger = _float(value, "--linger")
225
+ elif name == "idle":
226
+ options.idle = _float(value, "--idle")
227
+ elif name == "timeout":
228
+ options.timeout = _float(value, "--timeout")
229
+ elif name == "startup-timeout":
230
+ options.startup_timeout = _float(value, "--startup-timeout")
231
+ elif name == "analysis-timeout":
232
+ options.analysis_timeout = _float(value, "--analysis-timeout")
233
+ elif name == "assembler":
234
+ if value not in ("ghidra", "rasm2", "auto"):
235
+ raise UsageError("--assembler must be ghidra, rasm2 or auto")
236
+ options.assembler = value
237
+ elif name == "listen":
238
+ options.listen = value
239
+ elif name == "color":
240
+ options.color = _int(value, "--color")
241
+ elif name == "endian":
242
+ if value not in ("big", "little"):
243
+ raise UsageError("--endian must be big or little")
244
+ options.endian = value
245
+ elif name == "list-offset":
246
+ offset = _int(value, "--list-offset")
247
+ if offset < 0:
248
+ raise UsageError("--list-offset must not be negative")
249
+ options.list_offset = offset
250
+ elif name == "pipe-listen":
251
+ options.pipe_listen = _parse_pipe_listen(value)
252
+
253
+
254
+ def _apply_long_bool(options: Options, name: str) -> None:
255
+ if name == "help":
256
+ options.show_help = True
257
+ elif name == "version":
258
+ options.show_version = 1
259
+ elif name == "no-spawn":
260
+ options.spawn = False
261
+ elif name == "save-on-close":
262
+ options.save_on_close = True
263
+ elif name == "no-save-on-close":
264
+ options.save_on_close = False
265
+ elif name == "sessions":
266
+ options.list_sessions = True
267
+ elif name == "json":
268
+ options.json = True
269
+ elif name == "force-language":
270
+ options.force_language = True
271
+ elif name == "force-base":
272
+ options.force_base = True
273
+ elif name == "wait":
274
+ options.wait = True
275
+ elif name == "ephemeral":
276
+ options.ephemeral = True
277
+ elif name == "reimport":
278
+ options.reimport = True
279
+ elif name == "allow-exec":
280
+ options.allow_exec = True
281
+ elif name == "daemon-readonly":
282
+ options.daemon_readonly = True
283
+
284
+
285
+ def _short(options: Options, cluster: str, argv: list[str], index: int) -> int:
286
+ position = 0
287
+ while position < len(cluster):
288
+ doubled = cluster[position : position + 2]
289
+ if doubled in DOUBLED:
290
+ _apply_doubled(options, doubled)
291
+ position += 2
292
+ continue
293
+ char = cluster[position]
294
+ if char in REJECTED:
295
+ raise UsageError(f"-{char} is not supported by gdare2: {REJECTED[char]}")
296
+ if char in IGNORED:
297
+ options.ignored.append((f"-{char}", IGNORED[char]))
298
+ if char in IGNORED_VALUE:
299
+ rest = cluster[position + 1 :]
300
+ if rest:
301
+ return index
302
+ index += 1
303
+ _next(argv, index - 1, f"-{char}")
304
+ position += 1
305
+ continue
306
+ if char == "-":
307
+ break
308
+ if char in VALUE_FLAGS:
309
+ rest = cluster[position + 1 :]
310
+ if rest:
311
+ value = rest
312
+ else:
313
+ value = _next(argv, index, f"-{char}")
314
+ index += 1
315
+ _apply_short_value(options, char, value)
316
+ return index
317
+ if char in BOOLEAN_FLAGS:
318
+ _apply_short_flag(options, char)
319
+ position += 1
320
+ continue
321
+ raise UsageError(f"unknown option -{char}")
322
+ return index
323
+
324
+
325
+ def _apply_doubled(options: Options, doubled: str) -> None:
326
+ if doubled == "AA":
327
+ options.analyze = 2
328
+ elif doubled == "NN":
329
+ options.skip_rc = 2
330
+ elif doubled == "qq":
331
+ options.quiet = 2
332
+ elif doubled == "LL":
333
+ options.list_sessions = True
334
+ elif doubled == "nn":
335
+ options.ignored.append(("-nn", IGNORED["nn"]))
336
+
337
+
338
+ def _apply_short_value(options: Options, char: str, value: str) -> None:
339
+ if char == "a":
340
+ options.arch = value
341
+ elif char == "b":
342
+ options.bits = value
343
+ elif char in ("B", "m"):
344
+ options.base = value
345
+ elif char == "c":
346
+ options.commands.append(value)
347
+ elif char == "e":
348
+ key, separator, setting = value.partition("=")
349
+ if not separator:
350
+ raise UsageError(f"-e expects key=value, got {value!r}")
351
+ options.evals.append((key.strip(), setting.strip()))
352
+ elif char == "i":
353
+ options.post_scripts.append(value)
354
+ elif char == "I":
355
+ options.pre_scripts.append(value)
356
+ elif char == "k":
357
+ options.ignored.append(("-k", IGNORED["k"]))
358
+ elif char == "p":
359
+ options.project = value
360
+ elif char == "s":
361
+ options.seek = value
362
+ elif char == "t":
363
+ options.ignored.append(("-t", IGNORED["t"]))
364
+
365
+
366
+ def _apply_short_flag(options: Options, char: str) -> None:
367
+ if char == "A":
368
+ options.analyze = max(options.analyze, 1)
369
+ elif char == "h":
370
+ options.show_help = True
371
+ elif char == "j":
372
+ options.json = True
373
+ elif char == "L":
374
+ options.list_sessions = True
375
+ elif char == "n":
376
+ options.no_analysis = True
377
+ elif char == "N":
378
+ options.skip_rc = max(options.skip_rc, 1)
379
+ elif char == "q":
380
+ options.quiet = max(options.quiet, 1)
381
+ elif char == "v":
382
+ options.show_version = 1
383
+ elif char == "V":
384
+ options.show_version = 2
385
+ elif char == "w":
386
+ options.readonly = False
387
+
388
+
389
+ def _parse_pipe_listen(value: str) -> str:
390
+ """Validate ``--pipe-listen [host:]port`` and return it unchanged."""
391
+ host, _, port_text = value.rpartition(":")
392
+ if not port_text.isdigit():
393
+ raise UsageError("--pipe-listen expects [host:]port")
394
+ port = int(port_text)
395
+ if not 0 <= port <= 65535:
396
+ raise UsageError("--pipe-listen port must be 0-65535")
397
+ return value
398
+
399
+
400
+ def _next(argv: list[str], index: int, what: str) -> str:
401
+ if index >= len(argv):
402
+ raise UsageError(f"{what} needs a value")
403
+ return argv[index]
404
+
405
+
406
+ def _float(value: str, what: str) -> float:
407
+ try:
408
+ return float(value)
409
+ except ValueError:
410
+ raise UsageError(f"{what} needs a number, got {value!r}") from None
411
+
412
+
413
+ def _int(value: str, what: str) -> int:
414
+ try:
415
+ return int(value, 0)
416
+ except ValueError:
417
+ raise UsageError(f"{what} needs a number, got {value!r}") from None
gdare2/assembler.py ADDED
@@ -0,0 +1,157 @@
1
+ """Assembler backends: Ghidra SLEIGH and an optional rasm2 fallback."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import shutil
7
+ import subprocess
8
+
9
+ from gdare2.errors import GdarError, UnsupportedError, UsageError
10
+
11
+ MAX_LINES = 1024
12
+ RASM2_TIMEOUT = 15
13
+
14
+ #: gdare2 architecture -> (rasm2 -a name, big endian)
15
+ _RASM2_ARCH = {
16
+ "x86": ("x86", False),
17
+ "armle": ("arm", False),
18
+ "armbe": ("arm", True),
19
+ "aarch64": ("arm", False),
20
+ "aarch64be": ("arm", True),
21
+ "mipsle": ("mips", False),
22
+ "mipsbe": ("mips", True),
23
+ "mips64le": ("mips", False),
24
+ "mips64be": ("mips", True),
25
+ "ppcle": ("ppc", False),
26
+ "ppc": ("ppc", True),
27
+ "ppc64": ("ppc", True),
28
+ "ppc64le": ("ppc", False),
29
+ "riscv32": ("riscv", False),
30
+ "riscv64": ("riscv", False),
31
+ "sparc": ("sparc", True),
32
+ "sparc64": ("sparc", True),
33
+ "m68k": ("m68k", True),
34
+ "sh2": ("sh", True),
35
+ "sh2a": ("sh", True),
36
+ }
37
+
38
+ #: Ghidra language processors use upper-case names such as ARM/AARCH64; r2
39
+ #: aliases are lower-case. Map the canonical names as well so ELF imports
40
+ #: work with the rasm2 backend without requiring a raw-binary -a alias.
41
+ _RASM2_CANONICAL = {
42
+ "x86": "x86",
43
+ "arm": "arm",
44
+ "aarch64": "arm",
45
+ "mips": "mips",
46
+ "powerpc": "ppc",
47
+ "riscv": "riscv",
48
+ "sparc": "sparc",
49
+ "68000": "m68k",
50
+ "superh": "sh",
51
+ }
52
+
53
+ _HEXPAIRS = re.compile(r"^[0-9a-fA-F]+$")
54
+
55
+
56
+ def assemble(session, ea: int, text: str) -> dict:
57
+ """Assemble *text* using the configured backend."""
58
+ mode = session.config.get_str("asm.assembler")
59
+ if mode == "ghidra":
60
+ return session.call_assembler("assemble", ea=int(ea), text=text)
61
+ if mode == "rasm2":
62
+ return rasm2_assemble(session, int(ea), text)
63
+ if mode == "auto":
64
+ try:
65
+ return session.call_assembler("assemble", ea=int(ea), text=text)
66
+ except GdarError as original:
67
+ binary = session.config.get_str("asm.rasm2")
68
+ if shutil.which(binary) is None:
69
+ raise original
70
+ try:
71
+ payload = rasm2_assemble(session, int(ea), text)
72
+ except GdarError:
73
+ raise original from None
74
+ session.stderr("WARN: assembler fallback to rasm2")
75
+ return payload
76
+ raise UsageError("e asm.assembler must be ghidra, rasm2 or auto")
77
+
78
+
79
+ def assemble_write(session, ea: int, text: str) -> dict:
80
+ """Assemble and write, using one daemon call for the whole SLEIGH path."""
81
+ mode = session.config.get_str("asm.assembler")
82
+ if mode == "ghidra":
83
+ return session.call_assembler("assemble_write", ea=int(ea), text=text)
84
+ if mode == "auto":
85
+ try:
86
+ return session.call_assembler("assemble_write", ea=int(ea), text=text)
87
+ except GdarError as original:
88
+ binary = session.config.get_str("asm.rasm2")
89
+ if shutil.which(binary) is None:
90
+ raise original
91
+ try:
92
+ payload = rasm2_assemble(session, int(ea), text)
93
+ except GdarError:
94
+ raise original from None
95
+ session.stderr("WARN: assembler fallback to rasm2")
96
+ else:
97
+ payload = assemble(session, int(ea), text)
98
+ session.call("write_bytes", ea=int(ea), data=bytes(payload["bytes"]))
99
+ payload["addr"] = int(ea)
100
+ return payload
101
+
102
+
103
+ def rasm2_assemble(session, ea: int, text: str) -> dict:
104
+ binary = session.config.get_str("asm.rasm2")
105
+ arch = str(session.info.get("arch") or "")
106
+ mapping = _RASM2_ARCH.get(arch.lower())
107
+ if mapping is None:
108
+ canonical = _RASM2_CANONICAL.get(arch.lower())
109
+ if canonical is not None:
110
+ mapping = (canonical, False)
111
+ if mapping is None:
112
+ raise UnsupportedError(f"rasm2 has no architecture mapping for {arch!r}")
113
+ rasm_arch, big_endian = mapping
114
+ big_endian = big_endian or session.info.get("endian") == "big"
115
+ bits = int(session.info.get("bits") or 32)
116
+ lines = [line.strip() for line in text.splitlines() if line.strip()]
117
+ if not lines:
118
+ raise UsageError("assembly text is empty")
119
+ if len(lines) > MAX_LINES:
120
+ raise UsageError(f"assembly is limited to {MAX_LINES} lines")
121
+
122
+ address = int(ea)
123
+ items = []
124
+ for line in lines:
125
+ command = [binary, "-a", rasm_arch, "-b", str(bits)]
126
+ if big_endian:
127
+ command.append("-e")
128
+ command.append(line)
129
+ try:
130
+ result = subprocess.run(
131
+ command,
132
+ capture_output=True,
133
+ text=True,
134
+ timeout=RASM2_TIMEOUT,
135
+ check=False,
136
+ )
137
+ except FileNotFoundError:
138
+ raise UnsupportedError(f"rasm2 not found: {binary}") from None
139
+ except subprocess.TimeoutExpired:
140
+ raise GdarError(f"rasm2 timed out after {RASM2_TIMEOUT}s") from None
141
+ hexpairs = result.stdout.strip().splitlines()[-1] if result.stdout.strip() else ""
142
+ if result.returncode != 0 or not hexpairs or len(hexpairs) % 2 or not _HEXPAIRS.match(hexpairs):
143
+ detail = (result.stderr or result.stdout or "no output").strip().splitlines()
144
+ raise GdarError(
145
+ f"rasm2 failed for {line!r}: {detail[-1] if detail else 'no output'}"
146
+ )
147
+ data = bytes.fromhex(hexpairs)
148
+ items.append(
149
+ {"addr": address, "text": line, "bytes": data, "size": len(data)}
150
+ )
151
+ address += len(data)
152
+ return {
153
+ "items": items,
154
+ "bytes": b"".join(item["bytes"] for item in items),
155
+ "size": sum(item["size"] for item in items),
156
+ "assembler": "rasm2",
157
+ }