unidecompiler-cli 0.1.2__tar.gz → 0.2.0__tar.gz

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 (20) hide show
  1. unidecompiler_cli-0.2.0/PKG-INFO +120 -0
  2. unidecompiler_cli-0.2.0/README.md +104 -0
  3. {unidecompiler_cli-0.1.2 → unidecompiler_cli-0.2.0}/pyproject.toml +5 -4
  4. unidecompiler_cli-0.2.0/src/unidecompiler_cli/cli.py +366 -0
  5. unidecompiler_cli-0.2.0/src/unidecompiler_cli/templates.py +208 -0
  6. unidecompiler_cli-0.2.0/src/unidecompiler_cli.egg-info/PKG-INFO +120 -0
  7. {unidecompiler_cli-0.1.2 → unidecompiler_cli-0.2.0}/src/unidecompiler_cli.egg-info/SOURCES.txt +1 -0
  8. unidecompiler_cli-0.2.0/src/unidecompiler_cli.egg-info/requires.txt +4 -0
  9. unidecompiler_cli-0.1.2/PKG-INFO +0 -43
  10. unidecompiler_cli-0.1.2/README.md +0 -28
  11. unidecompiler_cli-0.1.2/src/unidecompiler_cli/cli.py +0 -144
  12. unidecompiler_cli-0.1.2/src/unidecompiler_cli.egg-info/PKG-INFO +0 -43
  13. unidecompiler_cli-0.1.2/src/unidecompiler_cli.egg-info/requires.txt +0 -3
  14. {unidecompiler_cli-0.1.2 → unidecompiler_cli-0.2.0}/setup.cfg +0 -0
  15. {unidecompiler_cli-0.1.2 → unidecompiler_cli-0.2.0}/src/unidecompiler_cli/__init__.py +0 -0
  16. {unidecompiler_cli-0.1.2 → unidecompiler_cli-0.2.0}/src/unidecompiler_cli/input_sources.py +0 -0
  17. {unidecompiler_cli-0.1.2 → unidecompiler_cli-0.2.0}/src/unidecompiler_cli/simulation.py +0 -0
  18. {unidecompiler_cli-0.1.2 → unidecompiler_cli-0.2.0}/src/unidecompiler_cli.egg-info/dependency_links.txt +0 -0
  19. {unidecompiler_cli-0.1.2 → unidecompiler_cli-0.2.0}/src/unidecompiler_cli.egg-info/entry_points.txt +0 -0
  20. {unidecompiler_cli-0.1.2 → unidecompiler_cli-0.2.0}/src/unidecompiler_cli.egg-info/top_level.txt +0 -0
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.4
2
+ Name: unidecompiler-cli
3
+ Version: 0.2.0
4
+ Summary: Command-line host for unidecompiler plugins
5
+ Author-email: Wker <1670133844@qq.com>
6
+ License-Expression: AGPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/Wker666/unidecompiler
8
+ Project-URL: Repository, https://github.com/Wker666/unidecompiler
9
+ Project-URL: Issues, https://github.com/Wker666/unidecompiler/issues
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: unidecompiler<0.3.0,>=0.2.0
13
+ Requires-Dist: unidecompiler-export<0.3.0,>=0.2.0
14
+ Requires-Dist: unidecompiler-simulator<0.3.0,>=0.2.0
15
+ Requires-Dist: unidecompiler-simulation-host-python<0.3.0,>=0.2.0
16
+
17
+ # unidecompiler-cli
18
+
19
+ `unidecompiler-cli` is the command-line host for installed `unidecompiler`
20
+ frontend plugins. It discovers plugins through the `unidecompiler.frontends`
21
+ entry-point group and uses the public `DecompilerEngine` facade.
22
+
23
+ Install the CLI and one or more frontend packages:
24
+
25
+ ```sh
26
+ python -m pip install unidecompiler-cli unidecompiler-plugin-python-pyc
27
+ ```
28
+
29
+ Run `unidecompiler --help` for command-line usage.
30
+
31
+ Progress is disabled by default so ordinary output remains clean for shell
32
+ pipelines. Enable the single-line two-level progress bar explicitly with
33
+ `--progress` (TTY auto mode) or `--progress always`; it is written only to
34
+ stderr. Use `--progress never` to make the disabled behavior explicit.
35
+
36
+ Use `-o/--output` to write one successful artifact to a file, or
37
+ `--output-dir` to export every successful artifact from a batch. Directory
38
+ exports use sanitized basenames and never overwrite an existing file; these
39
+ options leave stdout empty and report the destinations on stderr:
40
+
41
+ ```sh
42
+ unidecompiler sample.pyc --output sample.pseudocode.txt
43
+ unidecompiler samples/ --output-dir decompiled/
44
+ unidecompiler samples/ --format ast-json --output-dir ast/
45
+ ```
46
+
47
+ For a VS Code navigation sidecar, opt in explicitly and provide both paths.
48
+ The sidecar is written after the pseudocode file and is never generated by a
49
+ normal export. It is available only for one pseudocode `--output` export, not
50
+ for `--output-dir` or `--format ast-json`:
51
+
52
+ ```sh
53
+ unidecompiler sample.pyc \
54
+ --output sample.pseudocode.txt \
55
+ --vscode-metadata sample.pseudocode.txt.unidec.json
56
+ ```
57
+
58
+ The sidecar contains the exported text's UTF-8 SHA-256, UTF-16 source-map
59
+ offsets, and minimal instruction navigation facts. It does not repeat the
60
+ pseudocode or include source paths, AST, IR, CFG, diagnostics, or other engine
61
+ data.
62
+
63
+ Starter projects are exported through the host-side template package as well:
64
+
65
+ ```sh
66
+ unidecompiler template frontend MyVM -o ./my-vm \
67
+ --author "Your Name" \
68
+ --description "My bytecode frontend" \
69
+ --requirements "Decode and lift the VM" \
70
+ --suffix .vm --version 1
71
+
72
+ unidecompiler template gui_plugin MyPanel -o ./my-panel \
73
+ --author "Your Name" \
74
+ --description "A read-only GUI panel" \
75
+ --requirements "Show analysis data"
76
+ ```
77
+
78
+ The same template export is available as an interactive wizard:
79
+
80
+ ```sh
81
+ unidecompiler template --interactive
82
+ ```
83
+
84
+ The short form is `unidecompiler template -i`. The wizard only collects
85
+ template settings and then calls the same host-side exporter; it does not
86
+ change decompilation or recovery behavior.
87
+
88
+ Frontend templates can opt into the simulator and AI kit explicitly:
89
+
90
+ ```sh
91
+ unidecompiler template frontend MyVM -o ./my-vm \
92
+ --author A --description D --requirements R \
93
+ --suffix .vm --version 1 \
94
+ --simulation --ai-guidance \
95
+ --interpreter-source ./vm_interpreter.c \
96
+ --bytecode-sample ./sample.bin \
97
+ --entry-kind symbol --entry-value main
98
+ ```
99
+
100
+ Both options also accept `--no-simulation` and `--no-ai-guidance`; they are
101
+ disabled by default. AI guidance validates the supplied files for size,
102
+ regular-file status, and likely credentials before copying them.
103
+
104
+ `export-template` is accepted as an alias. Template export is atomic and never
105
+ overwrites an existing destination directory.
106
+
107
+ The optional simulator command is hosted here, while execution remains in the
108
+ separate simulator library:
109
+
110
+ ```sh
111
+ unidecompiler simulate sample.bytecode --function 'Example.run' --args '[1, 2]'
112
+ ```
113
+
114
+ For trusted programs that require functions outside the lifted module, pass a
115
+ Python environment file. Top-level functions are matched by name, while their
116
+ stdout and stderr are returned as structured simulation events:
117
+
118
+ ```sh
119
+ unidecompiler simulate sample.pyc --function main --environment runtime.py
120
+ ```
@@ -0,0 +1,104 @@
1
+ # unidecompiler-cli
2
+
3
+ `unidecompiler-cli` is the command-line host for installed `unidecompiler`
4
+ frontend plugins. It discovers plugins through the `unidecompiler.frontends`
5
+ entry-point group and uses the public `DecompilerEngine` facade.
6
+
7
+ Install the CLI and one or more frontend packages:
8
+
9
+ ```sh
10
+ python -m pip install unidecompiler-cli unidecompiler-plugin-python-pyc
11
+ ```
12
+
13
+ Run `unidecompiler --help` for command-line usage.
14
+
15
+ Progress is disabled by default so ordinary output remains clean for shell
16
+ pipelines. Enable the single-line two-level progress bar explicitly with
17
+ `--progress` (TTY auto mode) or `--progress always`; it is written only to
18
+ stderr. Use `--progress never` to make the disabled behavior explicit.
19
+
20
+ Use `-o/--output` to write one successful artifact to a file, or
21
+ `--output-dir` to export every successful artifact from a batch. Directory
22
+ exports use sanitized basenames and never overwrite an existing file; these
23
+ options leave stdout empty and report the destinations on stderr:
24
+
25
+ ```sh
26
+ unidecompiler sample.pyc --output sample.pseudocode.txt
27
+ unidecompiler samples/ --output-dir decompiled/
28
+ unidecompiler samples/ --format ast-json --output-dir ast/
29
+ ```
30
+
31
+ For a VS Code navigation sidecar, opt in explicitly and provide both paths.
32
+ The sidecar is written after the pseudocode file and is never generated by a
33
+ normal export. It is available only for one pseudocode `--output` export, not
34
+ for `--output-dir` or `--format ast-json`:
35
+
36
+ ```sh
37
+ unidecompiler sample.pyc \
38
+ --output sample.pseudocode.txt \
39
+ --vscode-metadata sample.pseudocode.txt.unidec.json
40
+ ```
41
+
42
+ The sidecar contains the exported text's UTF-8 SHA-256, UTF-16 source-map
43
+ offsets, and minimal instruction navigation facts. It does not repeat the
44
+ pseudocode or include source paths, AST, IR, CFG, diagnostics, or other engine
45
+ data.
46
+
47
+ Starter projects are exported through the host-side template package as well:
48
+
49
+ ```sh
50
+ unidecompiler template frontend MyVM -o ./my-vm \
51
+ --author "Your Name" \
52
+ --description "My bytecode frontend" \
53
+ --requirements "Decode and lift the VM" \
54
+ --suffix .vm --version 1
55
+
56
+ unidecompiler template gui_plugin MyPanel -o ./my-panel \
57
+ --author "Your Name" \
58
+ --description "A read-only GUI panel" \
59
+ --requirements "Show analysis data"
60
+ ```
61
+
62
+ The same template export is available as an interactive wizard:
63
+
64
+ ```sh
65
+ unidecompiler template --interactive
66
+ ```
67
+
68
+ The short form is `unidecompiler template -i`. The wizard only collects
69
+ template settings and then calls the same host-side exporter; it does not
70
+ change decompilation or recovery behavior.
71
+
72
+ Frontend templates can opt into the simulator and AI kit explicitly:
73
+
74
+ ```sh
75
+ unidecompiler template frontend MyVM -o ./my-vm \
76
+ --author A --description D --requirements R \
77
+ --suffix .vm --version 1 \
78
+ --simulation --ai-guidance \
79
+ --interpreter-source ./vm_interpreter.c \
80
+ --bytecode-sample ./sample.bin \
81
+ --entry-kind symbol --entry-value main
82
+ ```
83
+
84
+ Both options also accept `--no-simulation` and `--no-ai-guidance`; they are
85
+ disabled by default. AI guidance validates the supplied files for size,
86
+ regular-file status, and likely credentials before copying them.
87
+
88
+ `export-template` is accepted as an alias. Template export is atomic and never
89
+ overwrites an existing destination directory.
90
+
91
+ The optional simulator command is hosted here, while execution remains in the
92
+ separate simulator library:
93
+
94
+ ```sh
95
+ unidecompiler simulate sample.bytecode --function 'Example.run' --args '[1, 2]'
96
+ ```
97
+
98
+ For trusted programs that require functions outside the lifted module, pass a
99
+ Python environment file. Top-level functions are matched by name, while their
100
+ stdout and stderr are returned as structured simulation events:
101
+
102
+ ```sh
103
+ unidecompiler simulate sample.pyc --function main --environment runtime.py
104
+ ```
@@ -4,16 +4,17 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "unidecompiler-cli"
7
- version = "0.1.2"
7
+ version = "0.2.0"
8
8
  description = "Command-line host for unidecompiler plugins"
9
9
  readme = "README.md"
10
10
  license = "AGPL-3.0-or-later"
11
11
  authors = [{ name = "Wker", email = "1670133844@qq.com" }]
12
12
  requires-python = ">=3.11"
13
13
  dependencies = [
14
- "unidecompiler>=0.1.10,<0.2.0",
15
- "unidecompiler-simulator>=0.1.5,<0.2.0",
16
- "unidecompiler-simulation-host-python>=0.1.2,<0.2.0",
14
+ "unidecompiler>=0.2.0,<0.3.0",
15
+ "unidecompiler-export>=0.2.0,<0.3.0",
16
+ "unidecompiler-simulator>=0.2.0,<0.3.0",
17
+ "unidecompiler-simulation-host-python>=0.2.0,<0.3.0",
17
18
  ]
18
19
 
19
20
  [project.urls]
@@ -0,0 +1,366 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import base64
5
+ from dataclasses import fields, is_dataclass
6
+ import json
7
+ import math
8
+ import sys
9
+ from typing import Any
10
+ from pathlib import Path
11
+
12
+ from unidecompiler.engine import DecompilerEngine
13
+ from unidecompiler_export import (
14
+ VscodeMetadataExportError,
15
+ export_pseudocode_documents,
16
+ export_text_documents,
17
+ write_pseudocode,
18
+ write_pseudocode_with_vscode_metadata,
19
+ )
20
+ from unidecompiler.plugin_registry import FrontendRegistry
21
+ from unidecompiler.input_sources import expand_input_path
22
+ from unidecompiler.progress import ProgressEvent, ProgressReporter
23
+
24
+
25
+ def main(argv: list[str] | None = None, *, registry: FrontendRegistry | None = None) -> int:
26
+ command_argv = sys.argv[1:] if argv is None else argv
27
+ if command_argv and command_argv[0] in {"simulate", "template", "export-template"}:
28
+ if command_argv[0] in {"template", "export-template"}:
29
+ from unidecompiler_cli.templates import main as template_main
30
+
31
+ return template_main(command_argv[1:])
32
+ from unidecompiler_cli.simulation import main as simulate_main
33
+
34
+ return simulate_main(command_argv[1:], registry=registry)
35
+ parser = argparse.ArgumentParser(
36
+ prog="unidecompiler",
37
+ description="Decompile bytecode into generic pseudocode.",
38
+ epilog=(
39
+ "Commands:\n"
40
+ " template export a VM frontend or GUI plugin starter project\n"
41
+ " export-template alias for template\n"
42
+ " simulate run a recovered generic-IR function\n\n"
43
+ "Template examples:\n"
44
+ " unidecompiler template frontend MyVM -o ./my-vm --author NAME "
45
+ "--description TEXT --requirements TEXT --suffix .vm --version 1\n"
46
+ " unidecompiler template --interactive\n\n"
47
+ "Run 'unidecompiler template --help' for all template options."
48
+ ),
49
+ formatter_class=argparse.RawDescriptionHelpFormatter,
50
+ )
51
+ parser.add_argument("input", nargs="?", help="input bytecode file")
52
+ parser.add_argument(
53
+ "--frontend",
54
+ help="explicit frontend plugin id, e.g. lua",
55
+ default=None,
56
+ )
57
+ parser.add_argument(
58
+ "--versions",
59
+ action="store_true",
60
+ help="print supported frontend version matrix",
61
+ )
62
+ parser.add_argument(
63
+ "--format",
64
+ choices=("pseudocode", "ast-json"),
65
+ default="pseudocode",
66
+ help="output format (default: pseudocode)",
67
+ )
68
+ output_group = parser.add_mutually_exclusive_group()
69
+ output_group.add_argument(
70
+ "-o",
71
+ "--output",
72
+ type=Path,
73
+ help="write one successful result to this file instead of stdout",
74
+ )
75
+ output_group.add_argument(
76
+ "--output-dir",
77
+ type=Path,
78
+ help="write every successful result as a separate file in this directory",
79
+ )
80
+ parser.add_argument(
81
+ "--vscode-metadata",
82
+ type=Path,
83
+ metavar="PATH",
84
+ help="write an opt-in VS Code navigation sidecar; requires --output",
85
+ )
86
+ parser.add_argument(
87
+ "--progress",
88
+ nargs="?",
89
+ const="auto",
90
+ default="never",
91
+ metavar="MODE",
92
+ help="show a dynamic progress bar on stderr (auto, always, never; default: disabled)",
93
+ )
94
+ args = parser.parse_args(command_argv)
95
+ if args.progress not in {"auto", "always", "never"}:
96
+ # With an optional argument argparse otherwise treats the positional
97
+ # input after a bare ``--progress`` as MODE. Accept that natural
98
+ # spelling while retaining the explicit ``--progress always`` form.
99
+ if args.input is None:
100
+ args.input = args.progress
101
+ args.progress = "auto"
102
+ else:
103
+ parser.error("--progress MODE must be auto, always, or never")
104
+ if args.vscode_metadata is not None:
105
+ if args.output_dir is not None:
106
+ parser.error("--vscode-metadata cannot be used with --output-dir")
107
+ if args.format == "ast-json":
108
+ parser.error("--vscode-metadata is available only with --format pseudocode")
109
+ if args.output is None:
110
+ parser.error("--vscode-metadata requires --output PATH")
111
+
112
+ registry = registry or FrontendRegistry.discover()
113
+ if args.versions:
114
+ for plugin, support in registry.version_support():
115
+ versions = ", ".join(support.versions)
116
+ print(
117
+ f"{plugin.id}: {support.family} | {versions} | "
118
+ f"{support.status} | parser: {support.parser}"
119
+ )
120
+ return 0
121
+
122
+ if args.input is None:
123
+ parser.error("input is required unless --versions is used")
124
+ input_path = Path(args.input)
125
+ artifacts = expand_input_path(input_path)
126
+ if not artifacts:
127
+ raise SystemExit(f"no input files found in {input_path}")
128
+
129
+ ast_modules: list[dict[str, Any]] = []
130
+ successful_results = []
131
+ processed = 0
132
+ reporter: ProgressReporter | None = _CLIProgressReporter() if _progress_enabled(args.progress) else None
133
+ engine = DecompilerEngine.from_registry(registry)
134
+ results = engine.decompile_artifacts(
135
+ artifacts,
136
+ args.frontend,
137
+ progress=reporter,
138
+ )
139
+ for result in results:
140
+ if result.status == "resource":
141
+ print(f"resource: {result.display_path}", file=sys.stderr)
142
+ continue
143
+ if result.status != "ok":
144
+ detail = next(
145
+ (diagnostic.message for diagnostic in result.diagnostics if diagnostic.severity == "error"),
146
+ result.status,
147
+ )
148
+ print(f"error: {result.display_path}: {detail}", file=sys.stderr)
149
+ continue
150
+ if args.format == "ast-json":
151
+ if result.ast is not None:
152
+ ast_modules.append(_json_value(result.ast))
153
+ else:
154
+ if result.pseudocode is not None and args.output is None and args.output_dir is None:
155
+ print(result.pseudocode.text)
156
+ successful_results.append(result)
157
+ processed += 1
158
+
159
+ if processed == 0:
160
+ raise SystemExit(f"no supported input files found in {input_path}")
161
+ if args.output is not None:
162
+ if len(successful_results) != 1:
163
+ raise SystemExit("--output requires exactly one successful input artifact")
164
+ destination = args.output.expanduser()
165
+ destination.parent.mkdir(parents=True, exist_ok=True)
166
+ result = successful_results[0]
167
+ if args.format == "ast-json":
168
+ destination.write_text(
169
+ json.dumps(
170
+ {"schema_version": 1, "modules": [_json_value(result.ast)]},
171
+ ensure_ascii=False,
172
+ sort_keys=True,
173
+ allow_nan=False,
174
+ )
175
+ + "\n",
176
+ encoding="utf-8",
177
+ )
178
+ else:
179
+ assert result.pseudocode is not None
180
+ try:
181
+ if args.vscode_metadata is None:
182
+ write_pseudocode(result, destination)
183
+ else:
184
+ write_pseudocode_with_vscode_metadata(
185
+ result,
186
+ destination,
187
+ args.vscode_metadata.expanduser(),
188
+ )
189
+ except VscodeMetadataExportError as error:
190
+ raise SystemExit(str(error)) from error
191
+ except (OSError, ValueError) as error:
192
+ raise SystemExit(f"could not export pseudocode to {destination}: {error}") from error
193
+ print(f"exported: {destination}", file=sys.stderr)
194
+ return 0
195
+
196
+ if args.output_dir is not None:
197
+ directory = args.output_dir.expanduser()
198
+ directory.mkdir(parents=True, exist_ok=True)
199
+ if args.format == "ast-json":
200
+ exported = export_text_documents(
201
+ (
202
+ (
203
+ result.display_path,
204
+ json.dumps(
205
+ {"schema_version": 1, "modules": [_json_value(result.ast)]},
206
+ ensure_ascii=False,
207
+ sort_keys=True,
208
+ allow_nan=False,
209
+ )
210
+ + "\n",
211
+ )
212
+ for result in successful_results
213
+ if result.ast is not None
214
+ ),
215
+ directory,
216
+ suffix=".ast.json",
217
+ )
218
+ else:
219
+ exported = export_pseudocode_documents(successful_results, directory)
220
+ print(f"exported {len(exported)} file(s) to: {directory.resolve()}", file=sys.stderr)
221
+ return 0
222
+
223
+ if args.format == "ast-json":
224
+ print(
225
+ json.dumps(
226
+ {"schema_version": 1, "modules": ast_modules},
227
+ ensure_ascii=False,
228
+ sort_keys=True,
229
+ allow_nan=False,
230
+ )
231
+ )
232
+ return 0
233
+
234
+
235
+ def _progress_enabled(mode: str) -> bool:
236
+ return mode == "always" or (mode == "auto" and sys.stderr.isatty())
237
+
238
+
239
+ class _CLIProgressReporter:
240
+ """Render a two-level progress bar without writing to stdout.
241
+
242
+ The reporter deliberately uses only carriage-return updates. This keeps
243
+ pseudocode/JSON on stdout pipe-safe and avoids making the CLI depend on a
244
+ terminal UI library for a small host-side presentation concern.
245
+ """
246
+
247
+ _BAR_WIDTH = 24
248
+
249
+ def __init__(self, stream=None) -> None:
250
+ self._stream = sys.stderr if stream is None else stream
251
+ self._last_length = 0
252
+ self._finished = False
253
+
254
+ def report(self, event: ProgressEvent) -> None:
255
+ if self._finished:
256
+ return
257
+ line = self._format_line(event)
258
+ padding = max(0, self._last_length - len(line))
259
+ self._stream.write("\r" + line + (" " * padding))
260
+ self._stream.flush()
261
+ self._last_length = len(line)
262
+ if event.phase == "complete" and event.status in {"completed", "failed", "cancelled"}:
263
+ is_last = (
264
+ event.batch_index is None
265
+ or event.batch_total is None
266
+ or event.batch_index >= event.batch_total
267
+ )
268
+ if is_last:
269
+ self._stream.write("\n")
270
+ self._stream.flush()
271
+ self._finished = True
272
+
273
+ def _format_line(self, event: ProgressEvent) -> str:
274
+ batch_current, batch_total, batch_fraction = self._batch_values(event)
275
+ file_fraction = self._file_fraction(event)
276
+ batch = _bar(batch_fraction, self._BAR_WIDTH)
277
+ current = _bar(file_fraction, self._BAR_WIDTH)
278
+ batch_count = _count_text(batch_current, batch_total)
279
+ file_count = _work_count_text(event)
280
+ detail = event.message or event.phase
281
+ label = _truncate(event.artifact_label or "artifact", 64)
282
+ detail_text = f"{event.phase}: {detail} ({label})"
283
+ return f"Files [{batch}] {batch_count} | File [{current}] {file_count} | {detail_text}"
284
+
285
+ @staticmethod
286
+ def _batch_values(event: ProgressEvent) -> tuple[int | None, int | None, float | None]:
287
+ if event.batch_index is None or event.batch_total is None:
288
+ return None, None, None
289
+ total = event.batch_total
290
+ current = min(total, max(0, event.batch_index))
291
+ return current, total, (current / total if total > 0 else None)
292
+
293
+ @staticmethod
294
+ def _file_fraction(event: ProgressEvent) -> float | None:
295
+ if event.phase == "complete":
296
+ return 1.0 if event.status in {"completed", "failed", "cancelled"} else 0.0
297
+ if event.fraction is not None:
298
+ return event.fraction
299
+ if event.status == "completed":
300
+ return 1.0
301
+ if event.status == "started":
302
+ return 0.0
303
+ return None
304
+
305
+
306
+ def _bar(fraction: float | None, width: int) -> str:
307
+ if fraction is None:
308
+ return "?".ljust(width, "-")
309
+ filled = min(width, max(0, round(fraction * width)))
310
+ return "#" * filled + "-" * (width - filled)
311
+
312
+
313
+ def _count_text(current: int | None, total: int | None) -> str:
314
+ if current is None or total is None:
315
+ return "?/?"
316
+ return f"{current}/{total}"
317
+
318
+
319
+ def _work_count_text(event: ProgressEvent) -> str:
320
+ if event.completed is None or event.total is None:
321
+ return "?/?"
322
+ return f"{event.completed}/{event.total} {event.unit}"
323
+
324
+
325
+ def _truncate(value: str, limit: int) -> str:
326
+ if len(value) <= limit:
327
+ return value
328
+ if limit <= 1:
329
+ return value[:limit]
330
+ return value[: limit - 1] + "…"
331
+
332
+
333
+ def _json_value(value: Any) -> Any:
334
+ """Convert the generic AST dataclasses into a JSON-compatible tree."""
335
+ if is_dataclass(value):
336
+ return {
337
+ "node_type": type(value).__name__,
338
+ **{field.name: _json_value(getattr(value, field.name)) for field in fields(value)},
339
+ }
340
+ if isinstance(value, bytes | bytearray | memoryview):
341
+ return {
342
+ "value_type": "bytes",
343
+ "base64": base64.b64encode(bytes(value)).decode("ascii"),
344
+ }
345
+ if isinstance(value, complex):
346
+ return {"value_type": "complex", "real": value.real, "imag": value.imag}
347
+ if isinstance(value, float) and not math.isfinite(value):
348
+ return {"value_type": "float", "value": repr(value)}
349
+ if isinstance(value, tuple | list):
350
+ return [_json_value(item) for item in value]
351
+ if isinstance(value, dict):
352
+ if all(isinstance(key, str) for key in value):
353
+ return {key: _json_value(item) for key, item in value.items()}
354
+ return {
355
+ "value_type": "map",
356
+ "entries": [[_json_value(key), _json_value(item)] for key, item in value.items()],
357
+ }
358
+ if isinstance(value, set | frozenset):
359
+ return {"value_type": "set", "items": [_json_value(item) for item in sorted(value, key=repr)]}
360
+ if value is None or isinstance(value, str | int | float | bool):
361
+ return value
362
+ raise TypeError(f"AST JSON cannot encode {type(value).__name__}")
363
+
364
+
365
+ if __name__ == "__main__":
366
+ raise SystemExit(main())
@@ -0,0 +1,208 @@
1
+ """CLI host for exporting frontend and GUI plugin starter projects."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from unidecompiler_export.templates import (
9
+ TemplateExportError,
10
+ TemplateRequest,
11
+ derive_project_names,
12
+ export_template,
13
+ )
14
+
15
+
16
+ def main(argv: list[str]) -> int:
17
+ parser = argparse.ArgumentParser(
18
+ prog="unidecompiler template",
19
+ description="Export a VM frontend or GUI plugin starter project.",
20
+ )
21
+ parser.add_argument(
22
+ "--interactive", "-i", action="store_true",
23
+ help="collect template settings through an interactive prompt",
24
+ )
25
+ parser.add_argument("kind", choices=("frontend", "gui_plugin"), nargs="?")
26
+ parser.add_argument("project_name", nargs="?")
27
+ parser.add_argument("--output-directory", "-o", type=Path)
28
+ parser.add_argument("--author")
29
+ parser.add_argument("--description")
30
+ parser.add_argument("--requirements")
31
+ parser.add_argument("--vm-name", default=None)
32
+ parser.add_argument("--suffix", dest="suffixes", action="append", default=[])
33
+ parser.add_argument("--version", dest="versions", action="append", default=[])
34
+ parser.add_argument(
35
+ "--simulation",
36
+ action=argparse.BooleanOptionalAction,
37
+ default=None,
38
+ help="include the optional data-only simulator adapter (frontend templates only; default: disabled)",
39
+ )
40
+ parser.add_argument(
41
+ "--ai-guidance",
42
+ action=argparse.BooleanOptionalAction,
43
+ default=None,
44
+ help="include the AI development kit and copied analysis inputs (frontend templates only; default: disabled)",
45
+ )
46
+ parser.add_argument(
47
+ "--interpreter-source",
48
+ type=Path,
49
+ help="VM interpreter source required by --ai-guidance",
50
+ )
51
+ parser.add_argument(
52
+ "--bytecode-sample",
53
+ type=Path,
54
+ help="bytecode sample required by --ai-guidance",
55
+ )
56
+ parser.add_argument(
57
+ "--entry-kind",
58
+ choices=("symbol", "offset", "exported_function"),
59
+ default="",
60
+ help="VM entry coordinate kind required by --ai-guidance",
61
+ )
62
+ parser.add_argument(
63
+ "--entry-value",
64
+ default="",
65
+ help="VM entry symbol, offset, or exported function required by --ai-guidance",
66
+ )
67
+ parser.add_argument(
68
+ "--entry-context",
69
+ default="",
70
+ help="optional initial cursor/base, arguments, and setup facts for AI guidance",
71
+ )
72
+ args = parser.parse_args(argv)
73
+ if args.interactive:
74
+ try:
75
+ _collect_interactive(args)
76
+ except (EOFError, KeyboardInterrupt):
77
+ print("template generation cancelled", file=sys.stderr)
78
+ return 130
79
+ else:
80
+ required_options = (
81
+ "kind",
82
+ "project_name",
83
+ "output_directory",
84
+ "author",
85
+ "description",
86
+ "requirements",
87
+ )
88
+ missing = [option for option in required_options if getattr(args, option) in (None, "")]
89
+ if missing:
90
+ parser.error("the following arguments are required in non-interactive mode: " + ", ".join(missing))
91
+
92
+ # BooleanOptionalAction uses None above only to let the interactive wizard
93
+ # apply its documented defaults. The exporter itself receives real bools.
94
+ args.simulation = bool(args.simulation)
95
+ args.ai_guidance = bool(args.ai_guidance)
96
+
97
+ try:
98
+ display_name, project_id, package_name = derive_project_names(args.project_name)
99
+ request = TemplateRequest(
100
+ kind=args.kind,
101
+ project_id=project_id,
102
+ package_name=package_name,
103
+ display_name=display_name,
104
+ author=args.author,
105
+ description=args.description,
106
+ requirements=args.requirements,
107
+ output_directory=args.output_directory,
108
+ vm_name=args.vm_name or display_name,
109
+ suffixes=tuple(args.suffixes),
110
+ versions=tuple(args.versions),
111
+ include_simulation=args.simulation,
112
+ include_ai_guidance=args.ai_guidance,
113
+ interpreter_source=args.interpreter_source,
114
+ bytecode_sample=args.bytecode_sample,
115
+ entry_kind=args.entry_kind,
116
+ entry_value=args.entry_value,
117
+ entry_context=args.entry_context,
118
+ )
119
+ destination = export_template(request)
120
+ except (TemplateExportError, OSError) as error:
121
+ parser.error(str(error))
122
+ print(destination)
123
+ return 0
124
+
125
+
126
+ def _collect_interactive(args: argparse.Namespace) -> None:
127
+ """Fill an argparse namespace from a small, dependency-free wizard."""
128
+ args.kind = args.kind or _ask_choice("Template kind", ("frontend", "gui_plugin"), "frontend")
129
+ args.project_name = args.project_name or _ask_required("Project name")
130
+ args.output_directory = args.output_directory or Path(_ask_required("Output directory"))
131
+ args.author = args.author or _ask_required("Author")
132
+ args.description = args.description or _ask_required("Description")
133
+ args.requirements = args.requirements or _ask_required("Requested feature / requirements")
134
+
135
+ if args.kind == "frontend":
136
+ args.vm_name = args.vm_name or _ask("VM display name", args.project_name)
137
+ if not args.suffixes:
138
+ args.suffixes = _ask_csv("Bytecode suffixes", ".vm")
139
+ if not args.versions:
140
+ args.versions = _ask_csv("Bytecode versions", "1")
141
+ if args.simulation is None:
142
+ args.simulation = _ask_yes_no("Include the optional simulator adapter", False)
143
+ if args.ai_guidance is None:
144
+ args.ai_guidance = _ask_yes_no("Include the AI development kit", False)
145
+ if args.ai_guidance:
146
+ args.interpreter_source = args.interpreter_source or Path(
147
+ _ask_required("VM interpreter source file")
148
+ )
149
+ args.bytecode_sample = args.bytecode_sample or Path(
150
+ _ask_required("Bytecode sample file")
151
+ )
152
+ args.entry_kind = args.entry_kind or _ask_choice(
153
+ "VM entry kind", ("symbol", "offset", "exported_function"), "symbol"
154
+ )
155
+ args.entry_value = args.entry_value or _ask_required("VM entry value")
156
+ args.entry_context = args.entry_context or _ask(
157
+ "Entry context (optional)", ""
158
+ )
159
+ def _ask(label: str, default: str | None = None) -> str:
160
+ suffix = f" [{default}]" if default else ""
161
+ value = _read_prompt(f"{label}{suffix}: ").strip()
162
+ return value or (default or "")
163
+
164
+
165
+ def _ask_required(label: str) -> str:
166
+ while True:
167
+ value = _ask(label)
168
+ if value:
169
+ return value
170
+ print(f"{label} is required.", file=sys.stderr)
171
+
172
+
173
+ def _ask_choice(label: str, choices: tuple[str, ...], default: str) -> str:
174
+ choices_text = "/".join(choices)
175
+ while True:
176
+ value = _ask(f"{label} ({choices_text})", default)
177
+ if value in choices:
178
+ return value
179
+ print(f"Choose one of: {choices_text}", file=sys.stderr)
180
+
181
+
182
+ def _ask_csv(label: str, default: str) -> list[str]:
183
+ while True:
184
+ values = [item.strip() for item in _ask(label, default).split(",") if item.strip()]
185
+ if values:
186
+ return values
187
+ print(f"{label} must contain at least one value.", file=sys.stderr)
188
+
189
+
190
+ def _ask_yes_no(label: str, default: bool) -> bool:
191
+ default_text = "Y/n" if default else "y/N"
192
+ while True:
193
+ value = _read_prompt(f"{label} ({default_text}): ").strip().lower()
194
+ if not value:
195
+ return default
196
+ if value in {"y", "yes"}:
197
+ return True
198
+ if value in {"n", "no"}:
199
+ return False
200
+ print("Please answer yes or no.", file=sys.stderr)
201
+
202
+
203
+ def _read_prompt(prompt: str) -> str:
204
+ print(prompt, end="", file=sys.stderr, flush=True)
205
+ value = sys.stdin.readline()
206
+ if value == "":
207
+ raise EOFError
208
+ return value.rstrip("\r\n")
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.4
2
+ Name: unidecompiler-cli
3
+ Version: 0.2.0
4
+ Summary: Command-line host for unidecompiler plugins
5
+ Author-email: Wker <1670133844@qq.com>
6
+ License-Expression: AGPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/Wker666/unidecompiler
8
+ Project-URL: Repository, https://github.com/Wker666/unidecompiler
9
+ Project-URL: Issues, https://github.com/Wker666/unidecompiler/issues
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: unidecompiler<0.3.0,>=0.2.0
13
+ Requires-Dist: unidecompiler-export<0.3.0,>=0.2.0
14
+ Requires-Dist: unidecompiler-simulator<0.3.0,>=0.2.0
15
+ Requires-Dist: unidecompiler-simulation-host-python<0.3.0,>=0.2.0
16
+
17
+ # unidecompiler-cli
18
+
19
+ `unidecompiler-cli` is the command-line host for installed `unidecompiler`
20
+ frontend plugins. It discovers plugins through the `unidecompiler.frontends`
21
+ entry-point group and uses the public `DecompilerEngine` facade.
22
+
23
+ Install the CLI and one or more frontend packages:
24
+
25
+ ```sh
26
+ python -m pip install unidecompiler-cli unidecompiler-plugin-python-pyc
27
+ ```
28
+
29
+ Run `unidecompiler --help` for command-line usage.
30
+
31
+ Progress is disabled by default so ordinary output remains clean for shell
32
+ pipelines. Enable the single-line two-level progress bar explicitly with
33
+ `--progress` (TTY auto mode) or `--progress always`; it is written only to
34
+ stderr. Use `--progress never` to make the disabled behavior explicit.
35
+
36
+ Use `-o/--output` to write one successful artifact to a file, or
37
+ `--output-dir` to export every successful artifact from a batch. Directory
38
+ exports use sanitized basenames and never overwrite an existing file; these
39
+ options leave stdout empty and report the destinations on stderr:
40
+
41
+ ```sh
42
+ unidecompiler sample.pyc --output sample.pseudocode.txt
43
+ unidecompiler samples/ --output-dir decompiled/
44
+ unidecompiler samples/ --format ast-json --output-dir ast/
45
+ ```
46
+
47
+ For a VS Code navigation sidecar, opt in explicitly and provide both paths.
48
+ The sidecar is written after the pseudocode file and is never generated by a
49
+ normal export. It is available only for one pseudocode `--output` export, not
50
+ for `--output-dir` or `--format ast-json`:
51
+
52
+ ```sh
53
+ unidecompiler sample.pyc \
54
+ --output sample.pseudocode.txt \
55
+ --vscode-metadata sample.pseudocode.txt.unidec.json
56
+ ```
57
+
58
+ The sidecar contains the exported text's UTF-8 SHA-256, UTF-16 source-map
59
+ offsets, and minimal instruction navigation facts. It does not repeat the
60
+ pseudocode or include source paths, AST, IR, CFG, diagnostics, or other engine
61
+ data.
62
+
63
+ Starter projects are exported through the host-side template package as well:
64
+
65
+ ```sh
66
+ unidecompiler template frontend MyVM -o ./my-vm \
67
+ --author "Your Name" \
68
+ --description "My bytecode frontend" \
69
+ --requirements "Decode and lift the VM" \
70
+ --suffix .vm --version 1
71
+
72
+ unidecompiler template gui_plugin MyPanel -o ./my-panel \
73
+ --author "Your Name" \
74
+ --description "A read-only GUI panel" \
75
+ --requirements "Show analysis data"
76
+ ```
77
+
78
+ The same template export is available as an interactive wizard:
79
+
80
+ ```sh
81
+ unidecompiler template --interactive
82
+ ```
83
+
84
+ The short form is `unidecompiler template -i`. The wizard only collects
85
+ template settings and then calls the same host-side exporter; it does not
86
+ change decompilation or recovery behavior.
87
+
88
+ Frontend templates can opt into the simulator and AI kit explicitly:
89
+
90
+ ```sh
91
+ unidecompiler template frontend MyVM -o ./my-vm \
92
+ --author A --description D --requirements R \
93
+ --suffix .vm --version 1 \
94
+ --simulation --ai-guidance \
95
+ --interpreter-source ./vm_interpreter.c \
96
+ --bytecode-sample ./sample.bin \
97
+ --entry-kind symbol --entry-value main
98
+ ```
99
+
100
+ Both options also accept `--no-simulation` and `--no-ai-guidance`; they are
101
+ disabled by default. AI guidance validates the supplied files for size,
102
+ regular-file status, and likely credentials before copying them.
103
+
104
+ `export-template` is accepted as an alias. Template export is atomic and never
105
+ overwrites an existing destination directory.
106
+
107
+ The optional simulator command is hosted here, while execution remains in the
108
+ separate simulator library:
109
+
110
+ ```sh
111
+ unidecompiler simulate sample.bytecode --function 'Example.run' --args '[1, 2]'
112
+ ```
113
+
114
+ For trusted programs that require functions outside the lifted module, pass a
115
+ Python environment file. Top-level functions are matched by name, while their
116
+ stdout and stderr are returned as structured simulation events:
117
+
118
+ ```sh
119
+ unidecompiler simulate sample.pyc --function main --environment runtime.py
120
+ ```
@@ -4,6 +4,7 @@ src/unidecompiler_cli/__init__.py
4
4
  src/unidecompiler_cli/cli.py
5
5
  src/unidecompiler_cli/input_sources.py
6
6
  src/unidecompiler_cli/simulation.py
7
+ src/unidecompiler_cli/templates.py
7
8
  src/unidecompiler_cli.egg-info/PKG-INFO
8
9
  src/unidecompiler_cli.egg-info/SOURCES.txt
9
10
  src/unidecompiler_cli.egg-info/dependency_links.txt
@@ -0,0 +1,4 @@
1
+ unidecompiler<0.3.0,>=0.2.0
2
+ unidecompiler-export<0.3.0,>=0.2.0
3
+ unidecompiler-simulator<0.3.0,>=0.2.0
4
+ unidecompiler-simulation-host-python<0.3.0,>=0.2.0
@@ -1,43 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: unidecompiler-cli
3
- Version: 0.1.2
4
- Summary: Command-line host for unidecompiler plugins
5
- Author-email: Wker <1670133844@qq.com>
6
- License-Expression: AGPL-3.0-or-later
7
- Project-URL: Homepage, https://github.com/Wker666/unidecompiler
8
- Project-URL: Repository, https://github.com/Wker666/unidecompiler
9
- Project-URL: Issues, https://github.com/Wker666/unidecompiler/issues
10
- Requires-Python: >=3.11
11
- Description-Content-Type: text/markdown
12
- Requires-Dist: unidecompiler<0.2.0,>=0.1.10
13
- Requires-Dist: unidecompiler-simulator<0.2.0,>=0.1.5
14
- Requires-Dist: unidecompiler-simulation-host-python<0.2.0,>=0.1.2
15
-
16
- # unidecompiler-cli
17
-
18
- `unidecompiler-cli` is the command-line host for installed `unidecompiler`
19
- frontend plugins. It discovers plugins through the `unidecompiler.frontends`
20
- entry-point group and uses the public `DecompilerEngine` facade.
21
-
22
- Install the CLI and one or more frontend packages:
23
-
24
- ```sh
25
- python -m pip install unidecompiler-cli unidecompiler-plugin-python-pyc
26
- ```
27
-
28
- Run `unidecompiler --help` for command-line usage.
29
-
30
- The optional simulator command is hosted here, while execution remains in the
31
- separate simulator library:
32
-
33
- ```sh
34
- unidecompiler simulate sample.bytecode --function 'Example.run' --args '[1, 2]'
35
- ```
36
-
37
- For trusted programs that require functions outside the lifted module, pass a
38
- Python environment file. Top-level functions are matched by name, while their
39
- stdout and stderr are returned as structured simulation events:
40
-
41
- ```sh
42
- unidecompiler simulate sample.pyc --function main --environment runtime.py
43
- ```
@@ -1,28 +0,0 @@
1
- # unidecompiler-cli
2
-
3
- `unidecompiler-cli` is the command-line host for installed `unidecompiler`
4
- frontend plugins. It discovers plugins through the `unidecompiler.frontends`
5
- entry-point group and uses the public `DecompilerEngine` facade.
6
-
7
- Install the CLI and one or more frontend packages:
8
-
9
- ```sh
10
- python -m pip install unidecompiler-cli unidecompiler-plugin-python-pyc
11
- ```
12
-
13
- Run `unidecompiler --help` for command-line usage.
14
-
15
- The optional simulator command is hosted here, while execution remains in the
16
- separate simulator library:
17
-
18
- ```sh
19
- unidecompiler simulate sample.bytecode --function 'Example.run' --args '[1, 2]'
20
- ```
21
-
22
- For trusted programs that require functions outside the lifted module, pass a
23
- Python environment file. Top-level functions are matched by name, while their
24
- stdout and stderr are returned as structured simulation events:
25
-
26
- ```sh
27
- unidecompiler simulate sample.pyc --function main --environment runtime.py
28
- ```
@@ -1,144 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import base64
5
- from dataclasses import fields, is_dataclass
6
- import json
7
- import math
8
- import sys
9
- from typing import Any
10
- from pathlib import Path
11
-
12
- from unidecompiler.backends.pseudocode import GenericPseudocodeBackend
13
- from unidecompiler.core.astify import module_to_ast
14
- from unidecompiler.plugin_registry import FrontendRegistry, FrontendSelectionError
15
- from unidecompiler.plugins import FrontendDecodeError
16
- from unidecompiler.input_sources import expand_input_path
17
-
18
-
19
- def main(argv: list[str] | None = None, *, registry: FrontendRegistry | None = None) -> int:
20
- command_argv = sys.argv[1:] if argv is None else argv
21
- if command_argv[:1] == ["simulate"]:
22
- from unidecompiler_cli.simulation import main as simulate_main
23
-
24
- return simulate_main(command_argv[1:], registry=registry)
25
- parser = argparse.ArgumentParser(
26
- prog="unidecompiler",
27
- description="Decompile bytecode into generic pseudocode.",
28
- )
29
- parser.add_argument("input", nargs="?", help="input bytecode file")
30
- parser.add_argument(
31
- "--frontend",
32
- help="explicit frontend plugin id, e.g. lua",
33
- default=None,
34
- )
35
- parser.add_argument(
36
- "--versions",
37
- action="store_true",
38
- help="print supported frontend version matrix",
39
- )
40
- parser.add_argument(
41
- "--format",
42
- choices=("pseudocode", "ast-json"),
43
- default="pseudocode",
44
- help="output format (default: pseudocode)",
45
- )
46
- args = parser.parse_args(command_argv)
47
-
48
- registry = registry or FrontendRegistry.discover()
49
- if args.versions:
50
- for plugin, support in registry.version_support():
51
- versions = ", ".join(support.versions)
52
- print(
53
- f"{plugin.id}: {support.family} | {versions} | "
54
- f"{support.status} | parser: {support.parser}"
55
- )
56
- return 0
57
-
58
- if args.input is None:
59
- parser.error("input is required unless --versions is used")
60
- input_path = Path(args.input)
61
- artifacts = expand_input_path(input_path)
62
- if not artifacts:
63
- raise SystemExit(f"no input files found in {input_path}")
64
-
65
- backend = GenericPseudocodeBackend() if args.format == "pseudocode" else None
66
- ast_modules: list[dict[str, Any]] = []
67
- processed = 0
68
- for artifact in artifacts:
69
- try:
70
- frontend = registry.select(
71
- artifact.data, artifact.display_path, explicit_id=args.frontend
72
- )
73
- except FrontendSelectionError:
74
- print(f"resource: {artifact.display_path}", file=sys.stderr)
75
- continue
76
- try:
77
- decoded = frontend.decode(artifact.data, artifact.display_path)
78
- except FrontendDecodeError:
79
- print(f"resource: {artifact.display_path}", file=sys.stderr)
80
- continue
81
- try:
82
- module = frontend.lift(decoded)
83
- if args.format == "ast-json":
84
- ast_modules.append(_json_value(module_to_ast(module)))
85
- else:
86
- assert backend is not None
87
- emitted = backend.emit(module)
88
- print(emitted.text)
89
- except Exception as error:
90
- print(
91
- f"error: {artifact.display_path}: {type(error).__name__}: {error}",
92
- file=sys.stderr,
93
- )
94
- continue
95
- processed += 1
96
-
97
- if processed == 0:
98
- raise SystemExit(f"no supported input files found in {input_path}")
99
- if args.format == "ast-json":
100
- print(
101
- json.dumps(
102
- {"schema_version": 1, "modules": ast_modules},
103
- ensure_ascii=False,
104
- sort_keys=True,
105
- allow_nan=False,
106
- )
107
- )
108
- return 0
109
-
110
-
111
- def _json_value(value: Any) -> Any:
112
- """Convert the generic AST dataclasses into a JSON-compatible tree."""
113
- if is_dataclass(value):
114
- return {
115
- "node_type": type(value).__name__,
116
- **{field.name: _json_value(getattr(value, field.name)) for field in fields(value)},
117
- }
118
- if isinstance(value, bytes | bytearray | memoryview):
119
- return {
120
- "value_type": "bytes",
121
- "base64": base64.b64encode(bytes(value)).decode("ascii"),
122
- }
123
- if isinstance(value, complex):
124
- return {"value_type": "complex", "real": value.real, "imag": value.imag}
125
- if isinstance(value, float) and not math.isfinite(value):
126
- return {"value_type": "float", "value": repr(value)}
127
- if isinstance(value, tuple | list):
128
- return [_json_value(item) for item in value]
129
- if isinstance(value, dict):
130
- if all(isinstance(key, str) for key in value):
131
- return {key: _json_value(item) for key, item in value.items()}
132
- return {
133
- "value_type": "map",
134
- "entries": [[_json_value(key), _json_value(item)] for key, item in value.items()],
135
- }
136
- if isinstance(value, set | frozenset):
137
- return {"value_type": "set", "items": [_json_value(item) for item in sorted(value, key=repr)]}
138
- if value is None or isinstance(value, str | int | float | bool):
139
- return value
140
- raise TypeError(f"AST JSON cannot encode {type(value).__name__}")
141
-
142
-
143
- if __name__ == "__main__":
144
- raise SystemExit(main())
@@ -1,43 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: unidecompiler-cli
3
- Version: 0.1.2
4
- Summary: Command-line host for unidecompiler plugins
5
- Author-email: Wker <1670133844@qq.com>
6
- License-Expression: AGPL-3.0-or-later
7
- Project-URL: Homepage, https://github.com/Wker666/unidecompiler
8
- Project-URL: Repository, https://github.com/Wker666/unidecompiler
9
- Project-URL: Issues, https://github.com/Wker666/unidecompiler/issues
10
- Requires-Python: >=3.11
11
- Description-Content-Type: text/markdown
12
- Requires-Dist: unidecompiler<0.2.0,>=0.1.10
13
- Requires-Dist: unidecompiler-simulator<0.2.0,>=0.1.5
14
- Requires-Dist: unidecompiler-simulation-host-python<0.2.0,>=0.1.2
15
-
16
- # unidecompiler-cli
17
-
18
- `unidecompiler-cli` is the command-line host for installed `unidecompiler`
19
- frontend plugins. It discovers plugins through the `unidecompiler.frontends`
20
- entry-point group and uses the public `DecompilerEngine` facade.
21
-
22
- Install the CLI and one or more frontend packages:
23
-
24
- ```sh
25
- python -m pip install unidecompiler-cli unidecompiler-plugin-python-pyc
26
- ```
27
-
28
- Run `unidecompiler --help` for command-line usage.
29
-
30
- The optional simulator command is hosted here, while execution remains in the
31
- separate simulator library:
32
-
33
- ```sh
34
- unidecompiler simulate sample.bytecode --function 'Example.run' --args '[1, 2]'
35
- ```
36
-
37
- For trusted programs that require functions outside the lifted module, pass a
38
- Python environment file. Top-level functions are matched by name, while their
39
- stdout and stderr are returned as structured simulation events:
40
-
41
- ```sh
42
- unidecompiler simulate sample.pyc --function main --environment runtime.py
43
- ```
@@ -1,3 +0,0 @@
1
- unidecompiler<0.2.0,>=0.1.10
2
- unidecompiler-simulator<0.2.0,>=0.1.5
3
- unidecompiler-simulation-host-python<0.2.0,>=0.1.2