depyo 1.0.1 → 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 +4 -0
- package/depyo.js +60 -0
- package/lib/PycReader.js +29 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,6 +32,9 @@ node depyo.js --out /path/to/file.pyc
|
|
|
32
32
|
# Marshal-only blob (no .pyc header)
|
|
33
33
|
node depyo.js --marshal --py-version 3.11 /path/to/blob.bin
|
|
34
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
|
|
35
38
|
```
|
|
36
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.
|
|
37
40
|
|
|
@@ -45,6 +48,7 @@ Without `--py-version`, depyo scans supported versions (oldest → newest) and a
|
|
|
45
48
|
- `--skip-path` flatten output paths (write next to input)
|
|
46
49
|
- `--out` print source to stdout instead of files
|
|
47
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
|
|
48
52
|
- `--py-version <x.y>` bytecode version hint (use with `--marshal`)
|
|
49
53
|
- `--basedir <dir>` override output root (default: alongside input)
|
|
50
54
|
- `--file-ext <ext>` change emitted extension (default `py`)
|
package/depyo.js
CHANGED
|
@@ -21,6 +21,7 @@ global.g_cliArgs = {
|
|
|
21
21
|
skipPath: false,
|
|
22
22
|
sendToStdout: false,
|
|
23
23
|
marshal: false,
|
|
24
|
+
marshalScan: false,
|
|
24
25
|
pyVersion: null,
|
|
25
26
|
silent: false,
|
|
26
27
|
fileExt: 'py',
|
|
@@ -33,6 +34,7 @@ let g_totalOutThroughput = 0;
|
|
|
33
34
|
let g_totalExecTime = 0;
|
|
34
35
|
let g_totalFiles = 0;
|
|
35
36
|
let g_pyVersionInfo = null;
|
|
37
|
+
let g_marshalScanStats = {ok: 0, ambiguous: 0, failed: 0};
|
|
36
38
|
|
|
37
39
|
function printUsage() {
|
|
38
40
|
console.log(`Usage: node depyo.js [options] <file.pyc|archive.zip> [...]
|
|
@@ -48,6 +50,7 @@ Options:
|
|
|
48
50
|
--skip-path Flatten output paths (write files next to inputs)
|
|
49
51
|
--out Print decompiled source to stdout instead of files
|
|
50
52
|
--marshal Treat input as raw marshalled data (no .pyc header)
|
|
53
|
+
--marshal-scan Fast scan of marshal blobs (no decompile, prints version)
|
|
51
54
|
--py-version <x.y> Python bytecode version hint (auto-scan if omitted)
|
|
52
55
|
--basedir <path> Output base directory (default: alongside input)
|
|
53
56
|
--file-ext <ext> Extension for generated source (default: py)
|
|
@@ -77,6 +80,9 @@ function parseCLIParams() {
|
|
|
77
80
|
g_cliArgs.sendToStdout = true;
|
|
78
81
|
} else if (cliParam.toLowerCase() == "--marshal") {
|
|
79
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;
|
|
80
86
|
} else if (cliParam.toLowerCase() == "--py-version") {
|
|
81
87
|
g_cliArgs.pyVersion = process.argv[++idx];
|
|
82
88
|
} else if (cliParam.toLowerCase() == "--basedir") {
|
|
@@ -103,6 +109,44 @@ function normalizeMarshalOutput(src) {
|
|
|
103
109
|
.trim();
|
|
104
110
|
}
|
|
105
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
|
+
|
|
106
150
|
function attemptMarshalDecompile(buffer, versionInfo, opts = {}) {
|
|
107
151
|
const prevSilent = g_cliArgs.silent;
|
|
108
152
|
const prevDebug = g_cliArgs.debug;
|
|
@@ -195,6 +239,11 @@ function decompilePycObject(data) {
|
|
|
195
239
|
if (!Buffer.isBuffer(buffer)) {
|
|
196
240
|
buffer = fs.readFileSync(data);
|
|
197
241
|
}
|
|
242
|
+
if (g_cliArgs.marshalScan) {
|
|
243
|
+
const label = typeof data === 'string' ? data : '<buffer>';
|
|
244
|
+
scanMarshalBuffer(buffer, label);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
198
247
|
let rdr = null;
|
|
199
248
|
let pySrc = null;
|
|
200
249
|
let genSecs = 0;
|
|
@@ -369,6 +418,17 @@ g_baseDir = Path.resolve(baseInputDir, 'decompiled') + '/';
|
|
|
369
418
|
|
|
370
419
|
DecompileModule(g_cliArgs.filenames);
|
|
371
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
|
+
|
|
372
432
|
if (!g_cliArgs.sendToStdout) {
|
|
373
433
|
const inRate = (g_totalInThroughput / g_totalExecTime).toFixed(2);
|
|
374
434
|
const outRate = (g_totalOutThroughput / g_totalExecTime).toFixed(2);
|
package/lib/PycReader.js
CHANGED
|
@@ -280,6 +280,34 @@ class PycReader
|
|
|
280
280
|
return best ? best.candidate : null;
|
|
281
281
|
}
|
|
282
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
|
+
|
|
283
311
|
static CountUnknownOpcodes(codeObject, reader, opCodeList) {
|
|
284
312
|
const code = codeObject?.Code?.Value;
|
|
285
313
|
if (!code || !opCodeList) {
|
|
@@ -356,7 +384,7 @@ class PycReader
|
|
|
356
384
|
if (obj.Names?.ClassName === "Py_Tuple") {
|
|
357
385
|
score += 1;
|
|
358
386
|
}
|
|
359
|
-
return {score, remaining, unknown, unknownRatio};
|
|
387
|
+
return {score, remaining, unknown, total, unknownRatio};
|
|
360
388
|
} catch (ex) {
|
|
361
389
|
return null;
|
|
362
390
|
}
|