depyo 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +97 -0
- package/depyo.js +213 -0
- package/lib/BinaryReader.js +153 -0
- package/lib/OpCode.js +90 -0
- package/lib/OpCodes.js +940 -0
- package/lib/PycDecompiler.js +2031 -0
- package/lib/PycDisassembler.js +55 -0
- package/lib/PycReader.js +905 -0
- package/lib/PycResult.js +82 -0
- package/lib/PythonObject.js +242 -0
- package/lib/Unpickle.js +173 -0
- package/lib/ast/ast_node.js +3442 -0
- package/lib/bytecode/python_1_0.js +116 -0
- package/lib/bytecode/python_1_1.js +116 -0
- package/lib/bytecode/python_1_3.js +119 -0
- package/lib/bytecode/python_1_4.js +121 -0
- package/lib/bytecode/python_1_5.js +120 -0
- package/lib/bytecode/python_1_6.js +124 -0
- package/lib/bytecode/python_2_0.js +137 -0
- package/lib/bytecode/python_2_1.js +142 -0
- package/lib/bytecode/python_2_2.js +147 -0
- package/lib/bytecode/python_2_3.js +145 -0
- package/lib/bytecode/python_2_4.js +147 -0
- package/lib/bytecode/python_2_5.js +147 -0
- package/lib/bytecode/python_2_6.js +147 -0
- package/lib/bytecode/python_2_7.js +151 -0
- package/lib/bytecode/python_3_0.js +132 -0
- package/lib/bytecode/python_3_1.js +135 -0
- package/lib/bytecode/python_3_10.js +312 -0
- package/lib/bytecode/python_3_11.js +284 -0
- package/lib/bytecode/python_3_12.js +327 -0
- package/lib/bytecode/python_3_13.js +173 -0
- package/lib/bytecode/python_3_14.js +177 -0
- package/lib/bytecode/python_3_2.js +136 -0
- package/lib/bytecode/python_3_3.js +136 -0
- package/lib/bytecode/python_3_4.js +137 -0
- package/lib/bytecode/python_3_5.js +149 -0
- package/lib/bytecode/python_3_6.js +153 -0
- package/lib/bytecode/python_3_7.js +292 -0
- package/lib/bytecode/python_3_8.js +294 -0
- package/lib/bytecode/python_3_9.js +296 -0
- package/lib/code_reader.js +146 -0
- package/lib/handlers/binary_ops.js +174 -0
- package/lib/handlers/collections_update.js +239 -0
- package/lib/handlers/comparisons.js +95 -0
- package/lib/handlers/context_managers.js +250 -0
- package/lib/handlers/control_flow_jumps.js +954 -0
- package/lib/handlers/exceptions_blocks.js +952 -0
- package/lib/handlers/formatting.js +31 -0
- package/lib/handlers/function_calls.js +496 -0
- package/lib/handlers/function_class_build.js +330 -0
- package/lib/handlers/generators_async.js +172 -0
- package/lib/handlers/imports.js +53 -0
- package/lib/handlers/load_store_names.js +711 -0
- package/lib/handlers/loop_iterator.js +318 -0
- package/lib/handlers/misc_other.js +1201 -0
- package/lib/handlers/pattern_matching.js +226 -0
- package/lib/handlers/stack_ops.js +280 -0
- package/lib/handlers/subscript_slice.js +394 -0
- package/lib/handlers/unary_ops.js +91 -0
- package/lib/handlers/unpack.js +141 -0
- package/lib/stack_history.js +63 -0
- package/lib/zip_reader.js +217 -0
- package/package.json +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2011-2024 Sergey Kuznetsov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# depyo — Python bytecode decompiler in Node.js
|
|
2
|
+
|
|
3
|
+
Depyo converts Python `.pyc` files (or archives of them) back to readable Python source. It aims for broad coverage (Python 1.0 through 3.14) and fast throughput, with fixtures for modern features (exception groups, pattern matching, walrus, f-strings, async, context managers).
|
|
4
|
+
|
|
5
|
+
## Why depyo?
|
|
6
|
+
- **Wide version coverage:** Opcode tables and expected outputs for Python 1.0–3.14, plus decompilation support for PyPy bytecode sets.
|
|
7
|
+
- **Modern features:** WITH_EXCEPT_START/PREP_RERAISE_STAR, async/await, walrus, match/case, f-strings, type params, dict/set merges.
|
|
8
|
+
- **Workflow friendly:** CLI options for asm dumps, raw spacing hints, raw `.pyc` preservation, and flattened output paths.
|
|
9
|
+
- **Verification harness:** `run-fixtures.js` and `run-matrix.js` compare decompiled output against expected fixtures across versions.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
- Global: `npm i -g depyo`
|
|
13
|
+
- One-off: `npx depyo <file.pyc>`
|
|
14
|
+
|
|
15
|
+
Node.js 20+ recommended (matches CI).
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Decompile a single .pyc
|
|
21
|
+
node depyo.js /path/to/file.pyc
|
|
22
|
+
|
|
23
|
+
# Decompile a zip of .pyc files, emit asm and keep raw bytes
|
|
24
|
+
node depyo.js --asm --raw my_archive.zip
|
|
25
|
+
|
|
26
|
+
# Write sources next to inputs (skip directory mirroring)
|
|
27
|
+
node depyo.js --skip-path /path/to/file.pyc
|
|
28
|
+
|
|
29
|
+
# Dump to stdout instead of files
|
|
30
|
+
node depyo.js --out /path/to/file.pyc
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### CLI options
|
|
34
|
+
- `--asm` emit `.pyasm` disassembly alongside source
|
|
35
|
+
- `--raw` emit raw `.pyc` next to output
|
|
36
|
+
- `--raw-spacing` preserve blank lines/comment gaps
|
|
37
|
+
- `--dump` dump marshalled object tree
|
|
38
|
+
- `--stats` print throughput stats
|
|
39
|
+
- `--skip-source-gen` skip writing `.py` (use with `--asm/--dump`)
|
|
40
|
+
- `--skip-path` flatten output paths (write next to input)
|
|
41
|
+
- `--out` print source to stdout instead of files
|
|
42
|
+
- `--basedir <dir>` override output root (default: alongside input)
|
|
43
|
+
- `--file-ext <ext>` change emitted extension (default `py`)
|
|
44
|
+
|
|
45
|
+
## Examples
|
|
46
|
+
- Disassemble only (no source): `node depyo.js --skip-source-gen --asm file.pyc`
|
|
47
|
+
- Keep raw + disassembly next to source: `node depyo.js --raw --asm path/to/file.pyc`
|
|
48
|
+
- Flatten outputs (helpful for bulk zips): `node depyo.js --skip-path archive.zip`
|
|
49
|
+
|
|
50
|
+
## Testing
|
|
51
|
+
- Smoke per version:
|
|
52
|
+
```bash
|
|
53
|
+
node scripts/run-fixtures.js --root test/bytecode_3.14 --pattern py314_with_except_star --fail-fast
|
|
54
|
+
node scripts/run-fixtures.js --root test/bytecode_3.6 --pattern py36_fstrings --fail-fast
|
|
55
|
+
```
|
|
56
|
+
- Matrix (all versions, optional PyPy):
|
|
57
|
+
```bash
|
|
58
|
+
node scripts/run-matrix.js # full sweep
|
|
59
|
+
node scripts/run-matrix.js --pattern py311_exception_groups --fail-fast
|
|
60
|
+
```
|
|
61
|
+
- Modern fixtures are generated via `test/generate_modern_tests.py` (Python 3.8+ on PATH).
|
|
62
|
+
|
|
63
|
+
## Support matrix
|
|
64
|
+
- Python 1.0–3.14 opcode tables with expected fixtures.
|
|
65
|
+
- Modern features: match/case, walrus, f-strings, exception groups, type params.
|
|
66
|
+
- PyPy bytecode sets decompile; expected files are not yet part of CI.
|
|
67
|
+
- Legacy CI smokes (1.x/2.7/3.0–3.6) are informational (`continue-on-error`); modern feature checks are blocking.
|
|
68
|
+
|
|
69
|
+
## Known limitations
|
|
70
|
+
- **Inline comprehensions (Python 3.12+):** PEP 709 inlines list/dict/set comprehensions into parent code objects. Depyo currently reconstructs these as for-loops rather than comprehension expressions. Functions, classes, match/case, exception handling, and other constructs work correctly.
|
|
71
|
+
|
|
72
|
+
## Contributing / DX tips
|
|
73
|
+
- Use `node scripts/run-fixtures.js --pattern <piece>` for fast repros.
|
|
74
|
+
- For full coverage, `node scripts/run-matrix.js --fail-fast` (optionally add `--pattern`).
|
|
75
|
+
- Enable `--raw-spacing` to inspect potential comment/blank-line gaps.
|
|
76
|
+
- `--stats` helps when profiling throughput.
|
|
77
|
+
|
|
78
|
+
Comments and docs are in English; output mirrors the target Python version syntax.
|
|
79
|
+
|
|
80
|
+
## Comparison snapshot (at a glance)
|
|
81
|
+
|
|
82
|
+
| Project | Supported versions | Modern features (match, walrus, f-strings, exc groups) | Delivery | Expected fixtures | Notes |
|
|
83
|
+
| ------------------ | --------------------------- | ------------------------------------------------------ | ------------ | ----------------- | ----------------------------------------- |
|
|
84
|
+
| depyo | 1.0–3.14 (PyPy decompiles) | Yes | npm/npx, CLI | Yes (1.0–3.14) | Node.js CLI, asm/raw-spacing options |
|
|
85
|
+
| uncompyle6/decompyle3 | 2.x–3.12+ (lag on 3.13/3.14) | Partial (depends on branch) | pip | Partial | Python-based, slower adoption of new ops |
|
|
86
|
+
| pycdc (C++) | Mostly 2.x–3.x (limited new) | Partial | source build | No | Fast, but modern coverage limited |
|
|
87
|
+
|
|
88
|
+
## Quick benchmark (informal)
|
|
89
|
+
- Machine: local Node 25, single-thread.
|
|
90
|
+
- Case: `py314_exception_groups.pyc` decompiled 50× in-process: ~5.3 ms total (≈0.1 ms per decompile).
|
|
91
|
+
Use `node depyo.js --stats <file.pyc>` for your environment.
|
|
92
|
+
|
|
93
|
+
## Promotion ideas (OSS)
|
|
94
|
+
- Announce on HN/Reddit (Show HN / r/Python) with npm/npx one-liners.
|
|
95
|
+
- Add to awesome lists (`awesome-python`, `awesome-reverse-engineering`).
|
|
96
|
+
- Provide asciinema/GIF of `npx depyo file.pyc` + `--asm`.
|
|
97
|
+
- Encourage contributions via Issues/Discussions and `help wanted` labels.
|
package/depyo.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const {PycReader} = require('./lib/PycReader')
|
|
4
|
+
const PycDecompiler = require('./lib/PycDecompiler');
|
|
5
|
+
const PycDisassembler = require('./lib/PycDisassembler');
|
|
6
|
+
const ZipReader = require('./lib/zip_reader');
|
|
7
|
+
const zlib = require('zlib');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const Path = require('path');
|
|
10
|
+
const PycResult = require('./lib/PycResult');
|
|
11
|
+
|
|
12
|
+
let g_baseDir = './decompiled/';
|
|
13
|
+
global.g_cliArgs = {
|
|
14
|
+
debug: false,
|
|
15
|
+
raw: false,
|
|
16
|
+
rawSpacing: false,
|
|
17
|
+
dump: false,
|
|
18
|
+
asm: false,
|
|
19
|
+
stats: false,
|
|
20
|
+
skipSource: false,
|
|
21
|
+
skipPath: false,
|
|
22
|
+
sendToStdout: false,
|
|
23
|
+
fileExt: 'py',
|
|
24
|
+
baseDir: null,
|
|
25
|
+
filenames: []
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let g_totalInThroughput = 0;
|
|
29
|
+
let g_totalOutThroughput = 0;
|
|
30
|
+
let g_totalExecTime = 0;
|
|
31
|
+
let g_totalFiles = 0;
|
|
32
|
+
|
|
33
|
+
function printUsage() {
|
|
34
|
+
console.log(`Usage: node depyo.js [options] <file.pyc|archive.zip> [...]
|
|
35
|
+
|
|
36
|
+
Options:
|
|
37
|
+
--asm Emit .pyasm disassembly alongside source
|
|
38
|
+
--debug Verbose logging during decompilation
|
|
39
|
+
--raw Save raw .pyc next to output
|
|
40
|
+
--raw-spacing Preserve blank lines (show potential comment gaps)
|
|
41
|
+
--dump Dump marshalled object tree (.dump)
|
|
42
|
+
--stats Print throughput stats
|
|
43
|
+
--skip-source-gen Do not emit .py source (useful with --asm/--dump)
|
|
44
|
+
--skip-path Flatten output paths (write files next to inputs)
|
|
45
|
+
--out Print decompiled source to stdout instead of files
|
|
46
|
+
--basedir <path> Output base directory (default: alongside input)
|
|
47
|
+
--file-ext <ext> Extension for generated source (default: py)
|
|
48
|
+
`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseCLIParams() {
|
|
52
|
+
for (let idx = 2; idx < process.argv.length; idx++ ) {
|
|
53
|
+
let cliParam = process.argv[idx];
|
|
54
|
+
if (cliParam.toLowerCase() == "--asm") {
|
|
55
|
+
g_cliArgs.asm = true;
|
|
56
|
+
} else if (cliParam.toLowerCase() == "--debug") {
|
|
57
|
+
g_cliArgs.debug = true;
|
|
58
|
+
} else if (cliParam.toLowerCase() == "--raw") {
|
|
59
|
+
g_cliArgs.raw = true;
|
|
60
|
+
} else if (cliParam.toLowerCase() == "--raw-spacing") {
|
|
61
|
+
g_cliArgs.rawSpacing = true;
|
|
62
|
+
} else if (cliParam.toLowerCase() == "--dump") {
|
|
63
|
+
g_cliArgs.dump = true;
|
|
64
|
+
} else if (cliParam.toLowerCase() == "--stats") {
|
|
65
|
+
g_cliArgs.stats = true;
|
|
66
|
+
} else if (cliParam.toLowerCase() == "--skip-source-gen") {
|
|
67
|
+
g_cliArgs.skipSource = true;
|
|
68
|
+
} else if (cliParam.toLowerCase() == "--skip-path") {
|
|
69
|
+
g_cliArgs.skipPath = true;
|
|
70
|
+
} else if (cliParam.toLowerCase() == "--out") {
|
|
71
|
+
g_cliArgs.sendToStdout = true;
|
|
72
|
+
} else if (cliParam.toLowerCase() == "--basedir") {
|
|
73
|
+
g_cliArgs.baseDir = process.argv[++idx];
|
|
74
|
+
} else if (cliParam.toLowerCase() == "--file-ext") {
|
|
75
|
+
g_cliArgs.fileExt = process.argv[++idx];
|
|
76
|
+
} else if (cliParam.toLowerCase() == "--help" || cliParam.toLowerCase() == "-h") {
|
|
77
|
+
printUsage();
|
|
78
|
+
process.exit(0);
|
|
79
|
+
} else {
|
|
80
|
+
if (cliParam && cliParam.trim().length > 0) {
|
|
81
|
+
g_cliArgs.filenames.push(cliParam);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function decompilePycObject(data) {
|
|
88
|
+
try
|
|
89
|
+
{
|
|
90
|
+
let filename = null, obj = null;
|
|
91
|
+
let startTS = process.hrtime.bigint();
|
|
92
|
+
let rdr = new PycReader(data);
|
|
93
|
+
try {
|
|
94
|
+
obj = rdr.ReadObject();
|
|
95
|
+
filename = g_baseDir + obj.FileName;
|
|
96
|
+
} catch (ex) {
|
|
97
|
+
if (ex instanceof PycReader.LoadError) {
|
|
98
|
+
// Save the binary file if it not already exists for future manual analysis.
|
|
99
|
+
if (!ex.FileName) {
|
|
100
|
+
if (global.g_cliArgs?.debug) {
|
|
101
|
+
console.log(`LoadError: ${ex.message} at position ${ex.position}`);
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
filename = g_baseDir + ex.FileName;
|
|
106
|
+
let dirPath = Path.dirname(filename);
|
|
107
|
+
if (!g_cliArgs.skipPath && !fs.existsSync(dirPath)) {
|
|
108
|
+
fs.mkdirSync(dirPath, {recursive: true});
|
|
109
|
+
}
|
|
110
|
+
let filenamePyc = filename.substring(0, filename.lastIndexOf('.')) + ".pyc";
|
|
111
|
+
fs.writeFileSync(filenamePyc, rdr.Reader);
|
|
112
|
+
console.log(`Error: ${ex.message}\nFile: ${filenamePyc}\nPosition: ${ex.position}`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
console.log(`Error: ${ex.message}\nStack:\n${ex.stacktrace}`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!g_cliArgs.sendToStdout) {
|
|
120
|
+
console.log(`Processing ${filename}...`);
|
|
121
|
+
}
|
|
122
|
+
let dirPath = Path.dirname(filename);
|
|
123
|
+
let filenameBase = filename.substring(0, filename.lastIndexOf('.'));
|
|
124
|
+
if (g_cliArgs.skipPath) {
|
|
125
|
+
filenameBase = "./" + filenameBase.substring(dirPath.length + 1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!g_cliArgs.sendToStdout) {
|
|
129
|
+
if (!g_cliArgs.skipPath && !fs.existsSync(dirPath)) {
|
|
130
|
+
fs.mkdirSync(dirPath, {recursive: true});
|
|
131
|
+
}
|
|
132
|
+
if (g_cliArgs.raw) {
|
|
133
|
+
fs.writeFileSync(filenameBase + ".pyc", rdr.Reader);
|
|
134
|
+
}
|
|
135
|
+
if (g_cliArgs.dump) {
|
|
136
|
+
fs.writeFileSync(filenameBase + ".dump", PycReader.DumpObject(obj));
|
|
137
|
+
}
|
|
138
|
+
if (g_cliArgs.asm) {
|
|
139
|
+
fs.writeFileSync(filenameBase + ".pyasm", PycDisassembler.Disassemble(rdr, obj));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
let genStartTS = process.hrtime.bigint();
|
|
143
|
+
let decompiler = new PycDecompiler(obj);
|
|
144
|
+
let ast = decompiler.decompile();
|
|
145
|
+
let pycResult = ast.codeFragment();
|
|
146
|
+
let pySrc = pycResult.toString();
|
|
147
|
+
if (!pySrc.endsWith("\n")) {
|
|
148
|
+
pySrc += "\n";
|
|
149
|
+
}
|
|
150
|
+
let genSecs = parseInt(process.hrtime.bigint() - genStartTS) / 1000000000;
|
|
151
|
+
if (g_cliArgs.sendToStdout) {
|
|
152
|
+
// console.log(`\n\n${filenameBase}.${g_cliArgs.fileExt}\n-------\n${pySrc}`);
|
|
153
|
+
console.log(pySrc);
|
|
154
|
+
} else {
|
|
155
|
+
fs.writeFileSync(filenameBase + "." + g_cliArgs.fileExt, pySrc);
|
|
156
|
+
}
|
|
157
|
+
let secs = parseInt(process.hrtime.bigint() - startTS) / 1000000000;
|
|
158
|
+
g_totalExecTime += secs;
|
|
159
|
+
let inThroughput = data.length / genSecs;
|
|
160
|
+
let outThroughput = pySrc.length / genSecs;
|
|
161
|
+
g_totalInThroughput += data.length;
|
|
162
|
+
g_totalOutThroughput += pySrc.length;
|
|
163
|
+
g_totalFiles++;
|
|
164
|
+
if (g_cliArgs.stats) {
|
|
165
|
+
console.log(`Done in ${genSecs.toFixed(3)}s. In: ${data.length} bytes (${inThroughput.toFixed(2)} B/s). Out: ${pySrc.length} bytes (${outThroughput.toFixed(2)} B/s).`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch (ex)
|
|
169
|
+
{
|
|
170
|
+
console.log(`EXCEPTION: ${ex}`);
|
|
171
|
+
if (g_cliArgs.debug) {
|
|
172
|
+
console.log(ex.stack);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
function DecompileModule(filenames)
|
|
179
|
+
{
|
|
180
|
+
for (let filename of filenames) {
|
|
181
|
+
try {
|
|
182
|
+
let zipReader = new ZipReader(filename);
|
|
183
|
+
if (zipReader.isZipFile()) {
|
|
184
|
+
for (let entry of zipReader.entries()) {
|
|
185
|
+
let uncompressedData = zlib.inflateSync(entry.data);
|
|
186
|
+
|
|
187
|
+
decompilePycObject(uncompressedData);
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
decompilePycObject(filename);
|
|
191
|
+
}
|
|
192
|
+
} catch (ex) {
|
|
193
|
+
console.log(`Processing of file ${filename} was unsuccessful due to: ${ex.message}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
parseCLIParams()
|
|
199
|
+
if (g_cliArgs.filenames.length === 0) {
|
|
200
|
+
printUsage();
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const baseInputDir = g_cliArgs.baseDir ? g_cliArgs.baseDir : Path.dirname(g_cliArgs.filenames[0] || '.');
|
|
205
|
+
g_baseDir = Path.resolve(baseInputDir, 'decompiled') + '/';
|
|
206
|
+
|
|
207
|
+
DecompileModule(g_cliArgs.filenames);
|
|
208
|
+
|
|
209
|
+
if (!g_cliArgs.sendToStdout) {
|
|
210
|
+
const inRate = (g_totalInThroughput / g_totalExecTime).toFixed(2);
|
|
211
|
+
const outRate = (g_totalOutThroughput / g_totalExecTime).toFixed(2);
|
|
212
|
+
console.log(`Processed ${g_totalFiles} files in ${g_totalExecTime.toFixed(3)}s. In: ${g_totalInThroughput} bytes (${inRate} B/s). Out: ${g_totalOutThroughput} bytes (${outRate} B/s).`);
|
|
213
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
class BinaryReader {
|
|
2
|
+
constructor(code) {
|
|
3
|
+
if (!code) {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
this._reader = Buffer.from(code);
|
|
8
|
+
this._pc = 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
get pc() {
|
|
12
|
+
return this._pc;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
set pc(pos) {
|
|
16
|
+
this._pc = pos;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get EOF() {
|
|
20
|
+
return this._pc >= this._reader.length;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get Reader() {
|
|
24
|
+
return this._reader;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
readChar() {
|
|
28
|
+
return String.fromCharCode([this.readByte()]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
readByte() {
|
|
32
|
+
let value = this._reader.readUInt8(this.pc, true);
|
|
33
|
+
this.pc++;
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
readUInt16() {
|
|
38
|
+
let value = this._reader.readUInt16LE(this.pc, true);
|
|
39
|
+
this.pc += 2;
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
readUInt16BE() {
|
|
44
|
+
let value = this._reader.readUInt16BE(this.pc, true);
|
|
45
|
+
this.pc += 2;
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
readInt16() {
|
|
50
|
+
let value = this._reader.readInt16LE(this.pc, true);
|
|
51
|
+
this.pc += 2;
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
readInt16BE() {
|
|
56
|
+
let value = this._reader.readInt16BE(this.pc, true);
|
|
57
|
+
this.pc += 2;
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
readInt32() {
|
|
62
|
+
let value = this._reader.readInt32LE(this.pc, true);
|
|
63
|
+
this.pc += 4;
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
readInt32BE() {
|
|
68
|
+
let value = this._reader.readInt32BE(this.pc, true);
|
|
69
|
+
this.pc += 4;
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
readUInt32() {
|
|
74
|
+
let value = this._reader.readUInt32LE(this.pc, true);
|
|
75
|
+
this.pc += 4;
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
readUInt32BE() {
|
|
80
|
+
let value = this._reader.readUInt32BE(this.pc, true);
|
|
81
|
+
this.pc += 4;
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
readLong() {
|
|
86
|
+
let high = this.readInt32()
|
|
87
|
+
let low = this.readInt32()
|
|
88
|
+
return {low, high};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
readLongBE() {
|
|
92
|
+
let high = this.readInt32BE()
|
|
93
|
+
let low = this.readInt32BE()
|
|
94
|
+
return {low, high};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
readULong() {
|
|
98
|
+
let high = this.readUInt()
|
|
99
|
+
let low = this.readUInt()
|
|
100
|
+
return {low, high};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
readULongBE() {
|
|
104
|
+
let high = this.readUIntBE()
|
|
105
|
+
let low = this.readUIntBE()
|
|
106
|
+
return {low, high};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
readFloat() {
|
|
110
|
+
let value = this._reader.readFloatLE(this.pc, true);
|
|
111
|
+
this.pc += 4;
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
readFloatBE() {
|
|
116
|
+
let value = this._reader.readFloatBE(this.pc, true);
|
|
117
|
+
this.pc += 4;
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
readDouble() {
|
|
122
|
+
let value = this._reader.readDoubleLE(this.pc, true);
|
|
123
|
+
this.pc += 8;
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
readDoubleBE() {
|
|
128
|
+
let value = this._reader.readDoubleBE(this.pc, true);
|
|
129
|
+
this.pc += 8;
|
|
130
|
+
return value;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
readBytes(length) {
|
|
134
|
+
if (this.pc + length > this._reader.length) {
|
|
135
|
+
throw Error("Requested more data than buffer holds");
|
|
136
|
+
}
|
|
137
|
+
let value = this._reader.slice(this.pc, this.pc + length);
|
|
138
|
+
this.pc += length;
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
readString(length) {
|
|
143
|
+
let value = this.readBytes(length).toString('utf8');
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = {BinaryReader};
|
|
149
|
+
|
|
150
|
+
// Registering classes in global scope for propoer class deserialization.
|
|
151
|
+
for (let className of Object.keys(module.exports)) {
|
|
152
|
+
global[className] = new module.exports[className]();
|
|
153
|
+
}
|
package/lib/OpCode.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
class OpCode {
|
|
2
|
+
OpCodeID = 0;
|
|
3
|
+
InstructionName = null;
|
|
4
|
+
HasArgument = false;
|
|
5
|
+
Argument = 0;
|
|
6
|
+
HasName = false;
|
|
7
|
+
Name = null;
|
|
8
|
+
HasJumpRelative = false;
|
|
9
|
+
HasJumpAbsolute = false;
|
|
10
|
+
HasNegativeOffset = false;
|
|
11
|
+
HasConstant = false;
|
|
12
|
+
Constant = null;
|
|
13
|
+
ConstantObject = null;
|
|
14
|
+
HasCompare = false;
|
|
15
|
+
CompareOperator = null;
|
|
16
|
+
HasLocal = false;
|
|
17
|
+
LocalName = null;
|
|
18
|
+
HasBinaryOp = false;
|
|
19
|
+
BinaryOp = null;
|
|
20
|
+
HasIntrisic1 = false;
|
|
21
|
+
Intrisic1 = null;
|
|
22
|
+
HasIntrisic2 = false;
|
|
23
|
+
Intrisic2 = null;
|
|
24
|
+
HasFree = false;
|
|
25
|
+
FreeName = null;
|
|
26
|
+
Offset = 0;
|
|
27
|
+
LineNo = 0;
|
|
28
|
+
CodeBlock = null;
|
|
29
|
+
InstructionIndex = -1;
|
|
30
|
+
Size = 2; // Default size for Python 3.6+ word-aligned instructions
|
|
31
|
+
|
|
32
|
+
constructor(opcode, name, opts) {
|
|
33
|
+
this.OpCodeID = opcode;
|
|
34
|
+
this.InstructionName = name;
|
|
35
|
+
|
|
36
|
+
if (opts && typeof(opts) == "object") {
|
|
37
|
+
for (let [key, value] of Object.entries(opts)) {
|
|
38
|
+
if (key in this) {
|
|
39
|
+
this[key] = value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get Prev() {
|
|
47
|
+
if (!this.CodeBlock || this.InstructionIndex < 1) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return this.CodeBlock.Instructions[this.InstructionIndex - 1];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get Next() {
|
|
54
|
+
if (!this.CodeBlock || this.InstructionIndex < 0 || this.InstructionIndex >= this.CodeBlock.Instructions.length - 1) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return this.CodeBlock.Instructions[this.InstructionIndex + 1];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get JumpTarget () {
|
|
61
|
+
if (this.HasJumpRelative) {
|
|
62
|
+
// Python < 3.6: 3-byte instructions (opcode + 2 arg bytes)
|
|
63
|
+
// Python 3.6+: 2-byte instructions (opcode + arg byte)
|
|
64
|
+
return this.Offset + this.Size + this.Argument;
|
|
65
|
+
} else if (this.HasJumpAbsolute) {
|
|
66
|
+
return this.Argument;
|
|
67
|
+
} else {
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get Label() {
|
|
73
|
+
return this.Name || this.LocalName || this.FreeName;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
Clone() {
|
|
77
|
+
let clone = new OpCode();
|
|
78
|
+
for (let [key, value] of Object.entries(this)) {
|
|
79
|
+
clone[key] = value;
|
|
80
|
+
}
|
|
81
|
+
return clone;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = OpCode;
|
|
86
|
+
|
|
87
|
+
// Registering classes in global scope for propoer class deserialization.
|
|
88
|
+
for (let className of Object.keys(module.exports)) {
|
|
89
|
+
global[className] = new module.exports[className]();
|
|
90
|
+
}
|