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.
- unidecompiler_cli-0.2.0/PKG-INFO +120 -0
- unidecompiler_cli-0.2.0/README.md +104 -0
- {unidecompiler_cli-0.1.2 → unidecompiler_cli-0.2.0}/pyproject.toml +5 -4
- unidecompiler_cli-0.2.0/src/unidecompiler_cli/cli.py +366 -0
- unidecompiler_cli-0.2.0/src/unidecompiler_cli/templates.py +208 -0
- unidecompiler_cli-0.2.0/src/unidecompiler_cli.egg-info/PKG-INFO +120 -0
- {unidecompiler_cli-0.1.2 → unidecompiler_cli-0.2.0}/src/unidecompiler_cli.egg-info/SOURCES.txt +1 -0
- unidecompiler_cli-0.2.0/src/unidecompiler_cli.egg-info/requires.txt +4 -0
- unidecompiler_cli-0.1.2/PKG-INFO +0 -43
- unidecompiler_cli-0.1.2/README.md +0 -28
- unidecompiler_cli-0.1.2/src/unidecompiler_cli/cli.py +0 -144
- unidecompiler_cli-0.1.2/src/unidecompiler_cli.egg-info/PKG-INFO +0 -43
- unidecompiler_cli-0.1.2/src/unidecompiler_cli.egg-info/requires.txt +0 -3
- {unidecompiler_cli-0.1.2 → unidecompiler_cli-0.2.0}/setup.cfg +0 -0
- {unidecompiler_cli-0.1.2 → unidecompiler_cli-0.2.0}/src/unidecompiler_cli/__init__.py +0 -0
- {unidecompiler_cli-0.1.2 → unidecompiler_cli-0.2.0}/src/unidecompiler_cli/input_sources.py +0 -0
- {unidecompiler_cli-0.1.2 → unidecompiler_cli-0.2.0}/src/unidecompiler_cli/simulation.py +0 -0
- {unidecompiler_cli-0.1.2 → unidecompiler_cli-0.2.0}/src/unidecompiler_cli.egg-info/dependency_links.txt +0 -0
- {unidecompiler_cli-0.1.2 → unidecompiler_cli-0.2.0}/src/unidecompiler_cli.egg-info/entry_points.txt +0 -0
- {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.
|
|
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.
|
|
15
|
-
"unidecompiler-
|
|
16
|
-
"unidecompiler-
|
|
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
|
+
```
|
{unidecompiler_cli-0.1.2 → unidecompiler_cli-0.2.0}/src/unidecompiler_cli.egg-info/SOURCES.txt
RENAMED
|
@@ -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
|
unidecompiler_cli-0.1.2/PKG-INFO
DELETED
|
@@ -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
|
-
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unidecompiler_cli-0.1.2 → unidecompiler_cli-0.2.0}/src/unidecompiler_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{unidecompiler_cli-0.1.2 → unidecompiler_cli-0.2.0}/src/unidecompiler_cli.egg-info/top_level.txt
RENAMED
|
File without changes
|