depyo 1.0.0 → 1.0.2
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/README.md +19 -0
- package/depyo.js +250 -27
- package/lib/PycDecompiler.js +8 -4
- package/lib/PycReader.js +201 -1
- package/lib/ast/ast_node.js +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,7 +28,15 @@ node depyo.js --skip-path /path/to/file.pyc
|
|
|
28
28
|
|
|
29
29
|
# Dump to stdout instead of files
|
|
30
30
|
node depyo.js --out /path/to/file.pyc
|
|
31
|
+
|
|
32
|
+
# Marshal-only blob (no .pyc header)
|
|
33
|
+
node depyo.js --marshal --py-version 3.11 /path/to/blob.bin
|
|
34
|
+
node depyo.js --marshal /path/to/blob.bin
|
|
35
|
+
|
|
36
|
+
# Fast marshal scan (no decompile)
|
|
37
|
+
node depyo.js --marshal-scan /path/to/blob.bin
|
|
31
38
|
```
|
|
39
|
+
Without `--py-version`, depyo scans supported versions (oldest → newest) and accepts the first clean output when all clean candidates agree. If outputs diverge (ambiguous), it stops and asks for `--py-version`. Use `--debug` to see scan results.
|
|
32
40
|
|
|
33
41
|
### CLI options
|
|
34
42
|
- `--asm` emit `.pyasm` disassembly alongside source
|
|
@@ -39,6 +47,9 @@ node depyo.js --out /path/to/file.pyc
|
|
|
39
47
|
- `--skip-source-gen` skip writing `.py` (use with `--asm/--dump`)
|
|
40
48
|
- `--skip-path` flatten output paths (write next to input)
|
|
41
49
|
- `--out` print source to stdout instead of files
|
|
50
|
+
- `--marshal` treat input as raw marshalled data (no .pyc header, auto-scan versions)
|
|
51
|
+
- `--marshal-scan` fast scan marshal blobs and print version candidates
|
|
52
|
+
- `--py-version <x.y>` bytecode version hint (use with `--marshal`)
|
|
42
53
|
- `--basedir <dir>` override output root (default: alongside input)
|
|
43
54
|
- `--file-ext <ext>` change emitted extension (default `py`)
|
|
44
55
|
|
|
@@ -58,6 +69,14 @@ node depyo.js --out /path/to/file.pyc
|
|
|
58
69
|
node scripts/run-matrix.js # full sweep
|
|
59
70
|
node scripts/run-matrix.js --pattern py311_exception_groups --fail-fast
|
|
60
71
|
```
|
|
72
|
+
- Marshal fixtures (headerless marshal blobs):
|
|
73
|
+
```bash
|
|
74
|
+
node scripts/run-marshal-fixtures.js
|
|
75
|
+
```
|
|
76
|
+
- Regenerate marshal fixtures:
|
|
77
|
+
```bash
|
|
78
|
+
node scripts/generate-marshal-fixtures.js --clean
|
|
79
|
+
```
|
|
61
80
|
- Modern fixtures are generated via `test/generate_modern_tests.py` (Python 3.8+ on PATH).
|
|
62
81
|
|
|
63
82
|
## Support matrix
|
package/depyo.js
CHANGED
|
@@ -20,6 +20,10 @@ global.g_cliArgs = {
|
|
|
20
20
|
skipSource: false,
|
|
21
21
|
skipPath: false,
|
|
22
22
|
sendToStdout: false,
|
|
23
|
+
marshal: false,
|
|
24
|
+
marshalScan: false,
|
|
25
|
+
pyVersion: null,
|
|
26
|
+
silent: false,
|
|
23
27
|
fileExt: 'py',
|
|
24
28
|
baseDir: null,
|
|
25
29
|
filenames: []
|
|
@@ -29,6 +33,8 @@ let g_totalInThroughput = 0;
|
|
|
29
33
|
let g_totalOutThroughput = 0;
|
|
30
34
|
let g_totalExecTime = 0;
|
|
31
35
|
let g_totalFiles = 0;
|
|
36
|
+
let g_pyVersionInfo = null;
|
|
37
|
+
let g_marshalScanStats = {ok: 0, ambiguous: 0, failed: 0};
|
|
32
38
|
|
|
33
39
|
function printUsage() {
|
|
34
40
|
console.log(`Usage: node depyo.js [options] <file.pyc|archive.zip> [...]
|
|
@@ -43,6 +49,9 @@ Options:
|
|
|
43
49
|
--skip-source-gen Do not emit .py source (useful with --asm/--dump)
|
|
44
50
|
--skip-path Flatten output paths (write files next to inputs)
|
|
45
51
|
--out Print decompiled source to stdout instead of files
|
|
52
|
+
--marshal Treat input as raw marshalled data (no .pyc header)
|
|
53
|
+
--marshal-scan Fast scan of marshal blobs (no decompile, prints version)
|
|
54
|
+
--py-version <x.y> Python bytecode version hint (auto-scan if omitted)
|
|
46
55
|
--basedir <path> Output base directory (default: alongside input)
|
|
47
56
|
--file-ext <ext> Extension for generated source (default: py)
|
|
48
57
|
`);
|
|
@@ -69,6 +78,13 @@ function parseCLIParams() {
|
|
|
69
78
|
g_cliArgs.skipPath = true;
|
|
70
79
|
} else if (cliParam.toLowerCase() == "--out") {
|
|
71
80
|
g_cliArgs.sendToStdout = true;
|
|
81
|
+
} else if (cliParam.toLowerCase() == "--marshal") {
|
|
82
|
+
g_cliArgs.marshal = true;
|
|
83
|
+
} else if (cliParam.toLowerCase() == "--marshal-scan" || cliParam.toLowerCase() == "--marshal-smoke") {
|
|
84
|
+
g_cliArgs.marshalScan = true;
|
|
85
|
+
g_cliArgs.marshal = true;
|
|
86
|
+
} else if (cliParam.toLowerCase() == "--py-version") {
|
|
87
|
+
g_cliArgs.pyVersion = process.argv[++idx];
|
|
72
88
|
} else if (cliParam.toLowerCase() == "--basedir") {
|
|
73
89
|
g_cliArgs.baseDir = process.argv[++idx];
|
|
74
90
|
} else if (cliParam.toLowerCase() == "--file-ext") {
|
|
@@ -84,37 +100,219 @@ function parseCLIParams() {
|
|
|
84
100
|
}
|
|
85
101
|
}
|
|
86
102
|
|
|
103
|
+
function normalizeMarshalOutput(src) {
|
|
104
|
+
return src
|
|
105
|
+
.replace(/\r\n/g, '\n')
|
|
106
|
+
.replace(/[ \t]+$/gm, '')
|
|
107
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
108
|
+
.replace(/#[^\n]*$/gm, '')
|
|
109
|
+
.trim();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function scanMarshalBuffer(buffer, filenameLabel) {
|
|
113
|
+
if (g_pyVersionInfo) {
|
|
114
|
+
const trial = PycReader.TryParseMarshal(buffer, g_pyVersionInfo);
|
|
115
|
+
if (!trial) {
|
|
116
|
+
g_marshalScanStats.failed++;
|
|
117
|
+
console.log(`${filenameLabel}: no parse with ${g_pyVersionInfo.major}.${g_pyVersionInfo.minor}`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
g_marshalScanStats.ok++;
|
|
121
|
+
console.log(`${filenameLabel}: forced ${g_pyVersionInfo.major}.${g_pyVersionInfo.minor} unknown=${trial.unknown}/${trial.total} remaining=${trial.remaining}`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const results = PycReader.ScanMarshalCandidates(buffer);
|
|
126
|
+
if (!results.length) {
|
|
127
|
+
g_marshalScanStats.failed++;
|
|
128
|
+
console.log(`${filenameLabel}: no candidates`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const best = results[0];
|
|
133
|
+
const ambiguous = results.filter(r =>
|
|
134
|
+
r.unknown === best.unknown &&
|
|
135
|
+
r.remaining === best.remaining &&
|
|
136
|
+
r.unknownRatio === best.unknownRatio
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
if (ambiguous.length > 1) {
|
|
140
|
+
g_marshalScanStats.ambiguous++;
|
|
141
|
+
const versions = ambiguous.map(r => `${r.versionInfo.major}.${r.versionInfo.minor}`).join(', ');
|
|
142
|
+
console.log(`${filenameLabel}: ambiguous candidates (${versions})`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
g_marshalScanStats.ok++;
|
|
147
|
+
console.log(`${filenameLabel}: best=${best.versionInfo.major}.${best.versionInfo.minor} unknown=${best.unknown}/${best.total} remaining=${best.remaining}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function attemptMarshalDecompile(buffer, versionInfo, opts = {}) {
|
|
151
|
+
const prevSilent = g_cliArgs.silent;
|
|
152
|
+
const prevDebug = g_cliArgs.debug;
|
|
153
|
+
if (opts.silent) {
|
|
154
|
+
g_cliArgs.silent = true;
|
|
155
|
+
g_cliArgs.debug = false;
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const rdr = new PycReader(buffer, {marshal: true, versionInfo});
|
|
159
|
+
const obj = rdr.ReadObject();
|
|
160
|
+
if (!obj || obj.ClassName !== "Py_CodeObject") {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
const opcodes = new versionInfo.opcode(obj);
|
|
164
|
+
const {unknown, total} = PycReader.CountUnknownOpcodes(obj, rdr, opcodes.OpCodeList);
|
|
165
|
+
const unknownRatio = total > 0 ? unknown / total : 1;
|
|
166
|
+
const remaining = rdr.m_rdr.Reader.length - rdr.m_rdr.pc;
|
|
167
|
+
|
|
168
|
+
const genStartTS = process.hrtime.bigint();
|
|
169
|
+
const decompiler = new PycDecompiler(obj);
|
|
170
|
+
const ast = decompiler.decompile();
|
|
171
|
+
const pycResult = ast.codeFragment();
|
|
172
|
+
const pySrc = pycResult.toString();
|
|
173
|
+
const genSecs = Number(process.hrtime.bigint() - genStartTS) / 1000000000;
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
reader: rdr,
|
|
177
|
+
obj,
|
|
178
|
+
pySrc,
|
|
179
|
+
genSecs,
|
|
180
|
+
cleanBuild: decompiler.cleanBuild,
|
|
181
|
+
unknown,
|
|
182
|
+
total,
|
|
183
|
+
unknownRatio,
|
|
184
|
+
remaining,
|
|
185
|
+
versionInfo
|
|
186
|
+
};
|
|
187
|
+
} catch (ex) {
|
|
188
|
+
if (opts.debug) {
|
|
189
|
+
console.log(`Marshal decompile failed for ${versionInfo.major}.${versionInfo.minor}: ${ex.message}`);
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
} finally {
|
|
193
|
+
g_cliArgs.silent = prevSilent;
|
|
194
|
+
g_cliArgs.debug = prevDebug;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function selectMarshalCandidate(buffer) {
|
|
199
|
+
const candidates = PycReader.ListSupportedVersions(false);
|
|
200
|
+
const attempts = [];
|
|
201
|
+
const cleanCandidates = [];
|
|
202
|
+
let baselineOutput = null;
|
|
203
|
+
let outputsDiverged = false;
|
|
204
|
+
for (const candidate of candidates) {
|
|
205
|
+
const result = attemptMarshalDecompile(buffer, candidate, {silent: true});
|
|
206
|
+
if (!result) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
attempts.push(result);
|
|
210
|
+
const isWorking = result.cleanBuild && result.unknown === 0 && result.remaining === 0;
|
|
211
|
+
if (isWorking) {
|
|
212
|
+
const normalized = normalizeMarshalOutput(result.pySrc || '');
|
|
213
|
+
cleanCandidates.push({...result, normalized});
|
|
214
|
+
if (baselineOutput === null) {
|
|
215
|
+
baselineOutput = normalized;
|
|
216
|
+
} else if (baselineOutput !== normalized) {
|
|
217
|
+
outputsDiverged = true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!cleanCandidates.length) {
|
|
223
|
+
return attempts.length ? {best: null, attempts, ambiguous: false} : null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (outputsDiverged) {
|
|
227
|
+
return {best: null, attempts, ambiguous: true, cleanCandidates};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {best: cleanCandidates[0], attempts, ambiguous: false};
|
|
231
|
+
}
|
|
232
|
+
|
|
87
233
|
function decompilePycObject(data) {
|
|
88
234
|
try
|
|
89
235
|
{
|
|
90
236
|
let filename = null, obj = null;
|
|
91
237
|
let startTS = process.hrtime.bigint();
|
|
92
|
-
let
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
238
|
+
let buffer = data;
|
|
239
|
+
if (!Buffer.isBuffer(buffer)) {
|
|
240
|
+
buffer = fs.readFileSync(data);
|
|
241
|
+
}
|
|
242
|
+
if (g_cliArgs.marshalScan) {
|
|
243
|
+
const label = typeof data === 'string' ? data : '<buffer>';
|
|
244
|
+
scanMarshalBuffer(buffer, label);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
let rdr = null;
|
|
248
|
+
let pySrc = null;
|
|
249
|
+
let genSecs = 0;
|
|
250
|
+
|
|
251
|
+
if (g_cliArgs.marshal) {
|
|
252
|
+
let attemptResult = null;
|
|
253
|
+
if (g_pyVersionInfo) {
|
|
254
|
+
attemptResult = attemptMarshalDecompile(buffer, g_pyVersionInfo);
|
|
255
|
+
} else {
|
|
256
|
+
const scan = selectMarshalCandidate(buffer);
|
|
257
|
+
attemptResult = scan ? scan.best : null;
|
|
258
|
+
if (g_cliArgs.debug && scan?.attempts?.length) {
|
|
259
|
+
console.log("Marshal scan results:");
|
|
260
|
+
for (const attempt of scan.attempts) {
|
|
261
|
+
const info = attempt.versionInfo;
|
|
262
|
+
console.log(` ${info.major}.${info.minor}: clean=${attempt.cleanBuild} unknown=${attempt.unknown}/${attempt.total} remaining=${attempt.remaining}`);
|
|
102
263
|
}
|
|
103
|
-
|
|
264
|
+
if (attemptResult) {
|
|
265
|
+
const info = attemptResult.versionInfo;
|
|
266
|
+
console.log(`Selected marshal version: ${info.major}.${info.minor}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (!attemptResult && scan?.ambiguous) {
|
|
270
|
+
const versions = scan.cleanCandidates
|
|
271
|
+
? scan.cleanCandidates.map(c => `${c.versionInfo.major}.${c.versionInfo.minor}`).join(', ')
|
|
272
|
+
: 'unknown';
|
|
273
|
+
throw new Error(`Ambiguous marshal version (${versions}). Provide --py-version X.Y to force.`);
|
|
104
274
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!attemptResult) {
|
|
278
|
+
throw new Error("No clean marshal candidate found. Provide --py-version X.Y to force.");
|
|
279
|
+
}
|
|
280
|
+
rdr = attemptResult.reader;
|
|
281
|
+
obj = attemptResult.obj;
|
|
282
|
+
pySrc = attemptResult.pySrc;
|
|
283
|
+
genSecs = attemptResult.genSecs;
|
|
284
|
+
} else {
|
|
285
|
+
rdr = new PycReader(buffer);
|
|
286
|
+
try {
|
|
287
|
+
obj = rdr.ReadObject();
|
|
288
|
+
} catch (ex) {
|
|
289
|
+
if (ex instanceof PycReader.LoadError) {
|
|
290
|
+
// Save the binary file if it not already exists for future manual analysis.
|
|
291
|
+
if (!ex.FileName) {
|
|
292
|
+
if (global.g_cliArgs?.debug) {
|
|
293
|
+
console.log(`LoadError: ${ex.message} at position ${ex.position}`);
|
|
294
|
+
}
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
filename = g_baseDir + ex.FileName;
|
|
298
|
+
let dirPath = Path.dirname(filename);
|
|
299
|
+
if (!g_cliArgs.skipPath && !fs.existsSync(dirPath)) {
|
|
300
|
+
fs.mkdirSync(dirPath, {recursive: true});
|
|
301
|
+
}
|
|
302
|
+
let filenamePyc = filename.substring(0, filename.lastIndexOf('.')) + ".pyc";
|
|
303
|
+
fs.writeFileSync(filenamePyc, rdr.Reader);
|
|
304
|
+
console.log(`Error: ${ex.message}\nFile: ${filenamePyc}\nPosition: ${ex.position}`);
|
|
305
|
+
return;
|
|
109
306
|
}
|
|
110
|
-
|
|
111
|
-
fs.writeFileSync(filenamePyc, rdr.Reader);
|
|
112
|
-
console.log(`Error: ${ex.message}\nFile: ${filenamePyc}\nPosition: ${ex.position}`);
|
|
307
|
+
console.log(`Error: ${ex.message}\nStack:\n${ex.stacktrace}`);
|
|
113
308
|
return;
|
|
114
309
|
}
|
|
115
|
-
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!obj) {
|
|
116
313
|
return;
|
|
117
314
|
}
|
|
315
|
+
filename = g_baseDir + obj.FileName;
|
|
118
316
|
|
|
119
317
|
if (!g_cliArgs.sendToStdout) {
|
|
120
318
|
console.log(`Processing ${filename}...`);
|
|
@@ -139,15 +337,18 @@ function decompilePycObject(data) {
|
|
|
139
337
|
fs.writeFileSync(filenameBase + ".pyasm", PycDisassembler.Disassemble(rdr, obj));
|
|
140
338
|
}
|
|
141
339
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
340
|
+
if (!pySrc) {
|
|
341
|
+
let genStartTS = process.hrtime.bigint();
|
|
342
|
+
let decompiler = new PycDecompiler(obj);
|
|
343
|
+
let ast = decompiler.decompile();
|
|
344
|
+
let pycResult = ast.codeFragment();
|
|
345
|
+
pySrc = pycResult.toString();
|
|
346
|
+
genSecs = Number(process.hrtime.bigint() - genStartTS) / 1000000000;
|
|
347
|
+
}
|
|
147
348
|
if (!pySrc.endsWith("\n")) {
|
|
148
349
|
pySrc += "\n";
|
|
149
350
|
}
|
|
150
|
-
|
|
351
|
+
genSecs = Math.max(genSecs, 0.000000001);
|
|
151
352
|
if (g_cliArgs.sendToStdout) {
|
|
152
353
|
// console.log(`\n\n${filenameBase}.${g_cliArgs.fileExt}\n-------\n${pySrc}`);
|
|
153
354
|
console.log(pySrc);
|
|
@@ -156,9 +357,9 @@ function decompilePycObject(data) {
|
|
|
156
357
|
}
|
|
157
358
|
let secs = parseInt(process.hrtime.bigint() - startTS) / 1000000000;
|
|
158
359
|
g_totalExecTime += secs;
|
|
159
|
-
let inThroughput =
|
|
360
|
+
let inThroughput = buffer.length / genSecs;
|
|
160
361
|
let outThroughput = pySrc.length / genSecs;
|
|
161
|
-
g_totalInThroughput +=
|
|
362
|
+
g_totalInThroughput += buffer.length;
|
|
162
363
|
g_totalOutThroughput += pySrc.length;
|
|
163
364
|
g_totalFiles++;
|
|
164
365
|
if (g_cliArgs.stats) {
|
|
@@ -196,6 +397,17 @@ function DecompileModule(filenames)
|
|
|
196
397
|
}
|
|
197
398
|
|
|
198
399
|
parseCLIParams()
|
|
400
|
+
if (g_cliArgs.pyVersion && !g_cliArgs.marshal) {
|
|
401
|
+
console.log("Error: --py-version requires --marshal (headerless input).");
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
if (g_cliArgs.pyVersion) {
|
|
405
|
+
g_pyVersionInfo = PycReader.ResolveVersionTag(g_cliArgs.pyVersion);
|
|
406
|
+
if (!g_pyVersionInfo) {
|
|
407
|
+
console.log(`Error: unsupported --py-version "${g_cliArgs.pyVersion}". Use format X.Y (e.g., 3.11).`);
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
199
411
|
if (g_cliArgs.filenames.length === 0) {
|
|
200
412
|
printUsage();
|
|
201
413
|
process.exit(1);
|
|
@@ -206,6 +418,17 @@ g_baseDir = Path.resolve(baseInputDir, 'decompiled') + '/';
|
|
|
206
418
|
|
|
207
419
|
DecompileModule(g_cliArgs.filenames);
|
|
208
420
|
|
|
421
|
+
if (g_cliArgs.marshalScan) {
|
|
422
|
+
console.log(`Marshal scan summary: ok=${g_marshalScanStats.ok}, ambiguous=${g_marshalScanStats.ambiguous}, failed=${g_marshalScanStats.failed}`);
|
|
423
|
+
if (g_marshalScanStats.failed > 0) {
|
|
424
|
+
process.exit(1);
|
|
425
|
+
}
|
|
426
|
+
if (g_marshalScanStats.ambiguous > 0) {
|
|
427
|
+
process.exit(2);
|
|
428
|
+
}
|
|
429
|
+
process.exit(0);
|
|
430
|
+
}
|
|
431
|
+
|
|
209
432
|
if (!g_cliArgs.sendToStdout) {
|
|
210
433
|
const inRate = (g_totalInThroughput / g_totalExecTime).toFixed(2);
|
|
211
434
|
const outRate = (g_totalOutThroughput / g_totalExecTime).toFixed(2);
|
package/lib/PycDecompiler.js
CHANGED
|
@@ -712,7 +712,9 @@ class PycDecompiler {
|
|
|
712
712
|
{
|
|
713
713
|
PycDecompiler.opCodeHandlers[this.code.Current.OpCodeID].call(this);
|
|
714
714
|
} else {
|
|
715
|
-
|
|
715
|
+
if (!g_cliArgs?.silent) {
|
|
716
|
+
console.error(`Unsupported opcode ${this.code.Current.InstructionName} at pos ${this.code.Current.Offset}\n`);
|
|
717
|
+
}
|
|
716
718
|
this.cleanBuild = false;
|
|
717
719
|
let node = new AST.ASTNodeList(this.defBlock.nodes);
|
|
718
720
|
return node;
|
|
@@ -725,9 +727,11 @@ class PycDecompiler {
|
|
|
725
727
|
&& (this.curBlock.end == this.code.Next?.Offset);
|
|
726
728
|
|
|
727
729
|
} catch (ex) {
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
730
|
+
if (!g_cliArgs?.silent) {
|
|
731
|
+
console.error(`EXCEPTION for OpCode ${this.code.Current.InstructionName} (${this.code.Current.Argument}) at offset ${this.code.Current.Offset} in code object '${this.object.Name}', file offset ${this.object.codeOffset + this.code.Current.Offset} : ${ex.message}\n\n`);
|
|
732
|
+
if (global.g_cliArgs?.debug) {
|
|
733
|
+
console.error('Stack trace:', ex.stack);
|
|
734
|
+
}
|
|
731
735
|
}
|
|
732
736
|
}
|
|
733
737
|
}
|
package/lib/PycReader.js
CHANGED
|
@@ -103,6 +103,37 @@ const MagicToVersion = {
|
|
|
103
103
|
0x0A0D0E2B: {major: 3, minor: 14, IsUnicode: true, opcode: require('./bytecode/python_3_14')}
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
+
const VersionAliases = {
|
|
107
|
+
"1.2": "1.1"
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const VersionToInfo = {};
|
|
111
|
+
for (const [magic, info] of Object.entries(MagicToVersion)) {
|
|
112
|
+
const key = `${info.major}.${info.minor}`;
|
|
113
|
+
const existing = VersionToInfo[key];
|
|
114
|
+
const candidate = {...info, magic: Number(magic)};
|
|
115
|
+
if (!existing || (candidate.revision || 0) > (existing.revision || 0)) {
|
|
116
|
+
VersionToInfo[key] = candidate;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function parseVersionTag(tag) {
|
|
121
|
+
if (!tag) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const cleaned = String(tag).trim().replace(/^python/i, '').replace(/^py/i, '');
|
|
125
|
+
const match = cleaned.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/);
|
|
126
|
+
if (!match || match[2] === undefined) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const major = parseInt(match[1], 10);
|
|
130
|
+
const minor = parseInt(match[2], 10);
|
|
131
|
+
if (Number.isNaN(major) || Number.isNaN(minor)) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return {major, minor};
|
|
135
|
+
}
|
|
136
|
+
|
|
106
137
|
|
|
107
138
|
class PycReader
|
|
108
139
|
{
|
|
@@ -123,11 +154,14 @@ class PycReader
|
|
|
123
154
|
m_filename = null;
|
|
124
155
|
m_version = null;
|
|
125
156
|
|
|
126
|
-
constructor(data) {
|
|
157
|
+
constructor(data, options = {}) {
|
|
127
158
|
if (!data) {
|
|
128
159
|
return;
|
|
129
160
|
}
|
|
130
161
|
|
|
162
|
+
const opts = options || {};
|
|
163
|
+
this.m_filename = opts.filename || null;
|
|
164
|
+
|
|
131
165
|
let buffer = data;
|
|
132
166
|
if (typeof(data) == 'string') {
|
|
133
167
|
buffer = fs.readFileSync(data);
|
|
@@ -135,6 +169,21 @@ class PycReader
|
|
|
135
169
|
throw new Error('PycReader accepts only String as a file path or Buffer as content.');
|
|
136
170
|
}
|
|
137
171
|
this.m_rdr = new BinaryReader(buffer);
|
|
172
|
+
|
|
173
|
+
if (opts.marshal) {
|
|
174
|
+
let versionInfo = opts.versionInfo;
|
|
175
|
+
if (!versionInfo && opts.pyVersion) {
|
|
176
|
+
versionInfo = PycReader.ResolveVersionTag(opts.pyVersion);
|
|
177
|
+
}
|
|
178
|
+
if (!versionInfo) {
|
|
179
|
+
throw new Error('Marshal mode requires a bytecode version. Pass --py-version or pre-resolve it.');
|
|
180
|
+
}
|
|
181
|
+
this.m_version = versionInfo;
|
|
182
|
+
if (global.g_cliArgs?.debug && !opts.silent) {
|
|
183
|
+
console.log(`Marshal mode: Python ${this.m_version.major}.${this.m_version.minor}`);
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
138
187
|
|
|
139
188
|
let marshalVersion = this.m_rdr.readUInt32();
|
|
140
189
|
this.m_version = MagicToVersion[marshalVersion] || MagicToVersion[marshalVersion & 0xFFFFFFFE] || {major: -1, minor: -1, IsUnicode: false};
|
|
@@ -190,6 +239,157 @@ class PycReader
|
|
|
190
239
|
return this.m_version.opcode;
|
|
191
240
|
}
|
|
192
241
|
|
|
242
|
+
static ResolveVersionTag(tag) {
|
|
243
|
+
const parsed = parseVersionTag(tag);
|
|
244
|
+
if (!parsed) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
const key = `${parsed.major}.${parsed.minor}`;
|
|
248
|
+
const aliased = VersionAliases[key] ? VersionAliases[key] : key;
|
|
249
|
+
return VersionToInfo[aliased] || null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
static ListSupportedVersions(desc = true) {
|
|
253
|
+
const list = Object.values(VersionToInfo);
|
|
254
|
+
list.sort((a, b) => {
|
|
255
|
+
if (a.major !== b.major) {
|
|
256
|
+
return a.major - b.major;
|
|
257
|
+
}
|
|
258
|
+
return a.minor - b.minor;
|
|
259
|
+
});
|
|
260
|
+
return desc ? list.reverse() : list;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
static GuessVersion(buffer) {
|
|
264
|
+
const candidates = PycReader.ListSupportedVersions(true);
|
|
265
|
+
let best = null;
|
|
266
|
+
for (const candidate of candidates) {
|
|
267
|
+
const trial = PycReader.TryParseMarshal(buffer, candidate);
|
|
268
|
+
if (!trial) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (trial.unknown === 0 && trial.remaining === 0) {
|
|
272
|
+
return candidate;
|
|
273
|
+
}
|
|
274
|
+
if (!best ||
|
|
275
|
+
trial.unknownRatio < best.unknownRatio ||
|
|
276
|
+
(trial.unknownRatio === best.unknownRatio && trial.remaining < best.remaining)) {
|
|
277
|
+
best = {...trial, candidate};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return best ? best.candidate : null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
static ScanMarshalCandidates(buffer) {
|
|
284
|
+
const candidates = PycReader.ListSupportedVersions(false);
|
|
285
|
+
const results = [];
|
|
286
|
+
for (const candidate of candidates) {
|
|
287
|
+
const trial = PycReader.TryParseMarshal(buffer, candidate);
|
|
288
|
+
if (!trial) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
results.push({...trial, versionInfo: candidate});
|
|
292
|
+
}
|
|
293
|
+
results.sort((a, b) => {
|
|
294
|
+
if (a.unknownRatio !== b.unknownRatio) {
|
|
295
|
+
return a.unknownRatio - b.unknownRatio;
|
|
296
|
+
}
|
|
297
|
+
if (a.remaining !== b.remaining) {
|
|
298
|
+
return a.remaining - b.remaining;
|
|
299
|
+
}
|
|
300
|
+
if (a.unknown !== b.unknown) {
|
|
301
|
+
return a.unknown - b.unknown;
|
|
302
|
+
}
|
|
303
|
+
if (a.versionInfo.major !== b.versionInfo.major) {
|
|
304
|
+
return a.versionInfo.major - b.versionInfo.major;
|
|
305
|
+
}
|
|
306
|
+
return a.versionInfo.minor - b.versionInfo.minor;
|
|
307
|
+
});
|
|
308
|
+
return results;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
static CountUnknownOpcodes(codeObject, reader, opCodeList) {
|
|
312
|
+
const code = codeObject?.Code?.Value;
|
|
313
|
+
if (!code || !opCodeList) {
|
|
314
|
+
return {unknown: Number.MAX_SAFE_INTEGER, total: 0};
|
|
315
|
+
}
|
|
316
|
+
const wordSize = reader.getInstructionWordSize();
|
|
317
|
+
let unknown = 0;
|
|
318
|
+
let total = 0;
|
|
319
|
+
if (wordSize === 2) {
|
|
320
|
+
for (let offset = 0; offset < code.length; offset += 2) {
|
|
321
|
+
total++;
|
|
322
|
+
if (!opCodeList[code[offset]]) {
|
|
323
|
+
unknown++;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
} else {
|
|
327
|
+
let offset = 0;
|
|
328
|
+
while (offset < code.length) {
|
|
329
|
+
total++;
|
|
330
|
+
const opcode = code[offset];
|
|
331
|
+
const entry = opCodeList[opcode];
|
|
332
|
+
if (!entry) {
|
|
333
|
+
unknown++;
|
|
334
|
+
offset += 1;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
offset += entry.HasArgument ? 3 : 1;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return {unknown, total};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
static TryParseMarshal(buffer, versionInfo) {
|
|
344
|
+
try {
|
|
345
|
+
const reader = new PycReader(buffer, {marshal: true, versionInfo, silent: true});
|
|
346
|
+
const obj = reader.ReadObject();
|
|
347
|
+
if (!obj || obj.ClassName !== "Py_CodeObject") {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
const remaining = reader.m_rdr.Reader.length - reader.m_rdr.pc;
|
|
351
|
+
if (remaining < 0) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const prevDebug = global.g_cliArgs?.debug;
|
|
356
|
+
if (global.g_cliArgs) {
|
|
357
|
+
global.g_cliArgs.debug = false;
|
|
358
|
+
}
|
|
359
|
+
let opcodes = null;
|
|
360
|
+
try {
|
|
361
|
+
opcodes = new versionInfo.opcode(obj);
|
|
362
|
+
} finally {
|
|
363
|
+
if (global.g_cliArgs) {
|
|
364
|
+
global.g_cliArgs.debug = prevDebug;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const {unknown, total} = PycReader.CountUnknownOpcodes(obj, reader, opcodes?.OpCodeList);
|
|
369
|
+
const unknownRatio = total > 0 ? unknown / total : 1;
|
|
370
|
+
|
|
371
|
+
let score = 0;
|
|
372
|
+
if (remaining === 0) {
|
|
373
|
+
score += 3;
|
|
374
|
+
}
|
|
375
|
+
if (unknown === 0) {
|
|
376
|
+
score += 2;
|
|
377
|
+
}
|
|
378
|
+
if (obj.Code?.Value && Buffer.isBuffer(obj.Code.Value)) {
|
|
379
|
+
score += 1;
|
|
380
|
+
}
|
|
381
|
+
if (obj.Consts?.ClassName === "Py_Tuple") {
|
|
382
|
+
score += 1;
|
|
383
|
+
}
|
|
384
|
+
if (obj.Names?.ClassName === "Py_Tuple") {
|
|
385
|
+
score += 1;
|
|
386
|
+
}
|
|
387
|
+
return {score, remaining, unknown, total, unknownRatio};
|
|
388
|
+
} catch (ex) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
193
393
|
ReadObject() {
|
|
194
394
|
try {
|
|
195
395
|
let obj = new PythonObject(), value = null;
|
package/lib/ast/ast_node.js
CHANGED
|
@@ -1863,7 +1863,7 @@ class ASTKwNamesMap extends ASTNode {
|
|
|
1863
1863
|
|
|
1864
1864
|
constructor(values) {
|
|
1865
1865
|
super();
|
|
1866
|
-
this.m_values = values;
|
|
1866
|
+
this.m_values = Array.isArray(values) ? values : [];
|
|
1867
1867
|
}
|
|
1868
1868
|
|
|
1869
1869
|
get values() {
|
|
@@ -1876,6 +1876,9 @@ class ASTKwNamesMap extends ASTNode {
|
|
|
1876
1876
|
}
|
|
1877
1877
|
|
|
1878
1878
|
add (key, value) {
|
|
1879
|
+
if (!this.m_values) {
|
|
1880
|
+
this.m_values = [];
|
|
1881
|
+
}
|
|
1879
1882
|
this.values.push({key, value});
|
|
1880
1883
|
}
|
|
1881
1884
|
|