depyo 1.0.0 → 1.0.1
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 +15 -0
- package/depyo.js +190 -27
- package/lib/PycDecompiler.js +8 -4
- package/lib/PycReader.js +173 -1
- package/lib/ast/ast_node.js +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,7 +28,12 @@ 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
|
|
31
35
|
```
|
|
36
|
+
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
37
|
|
|
33
38
|
### CLI options
|
|
34
39
|
- `--asm` emit `.pyasm` disassembly alongside source
|
|
@@ -39,6 +44,8 @@ node depyo.js --out /path/to/file.pyc
|
|
|
39
44
|
- `--skip-source-gen` skip writing `.py` (use with `--asm/--dump`)
|
|
40
45
|
- `--skip-path` flatten output paths (write next to input)
|
|
41
46
|
- `--out` print source to stdout instead of files
|
|
47
|
+
- `--marshal` treat input as raw marshalled data (no .pyc header, auto-scan versions)
|
|
48
|
+
- `--py-version <x.y>` bytecode version hint (use with `--marshal`)
|
|
42
49
|
- `--basedir <dir>` override output root (default: alongside input)
|
|
43
50
|
- `--file-ext <ext>` change emitted extension (default `py`)
|
|
44
51
|
|
|
@@ -58,6 +65,14 @@ node depyo.js --out /path/to/file.pyc
|
|
|
58
65
|
node scripts/run-matrix.js # full sweep
|
|
59
66
|
node scripts/run-matrix.js --pattern py311_exception_groups --fail-fast
|
|
60
67
|
```
|
|
68
|
+
- Marshal fixtures (headerless marshal blobs):
|
|
69
|
+
```bash
|
|
70
|
+
node scripts/run-marshal-fixtures.js
|
|
71
|
+
```
|
|
72
|
+
- Regenerate marshal fixtures:
|
|
73
|
+
```bash
|
|
74
|
+
node scripts/generate-marshal-fixtures.js --clean
|
|
75
|
+
```
|
|
61
76
|
- Modern fixtures are generated via `test/generate_modern_tests.py` (Python 3.8+ on PATH).
|
|
62
77
|
|
|
63
78
|
## Support matrix
|
package/depyo.js
CHANGED
|
@@ -20,6 +20,9 @@ global.g_cliArgs = {
|
|
|
20
20
|
skipSource: false,
|
|
21
21
|
skipPath: false,
|
|
22
22
|
sendToStdout: false,
|
|
23
|
+
marshal: false,
|
|
24
|
+
pyVersion: null,
|
|
25
|
+
silent: false,
|
|
23
26
|
fileExt: 'py',
|
|
24
27
|
baseDir: null,
|
|
25
28
|
filenames: []
|
|
@@ -29,6 +32,7 @@ let g_totalInThroughput = 0;
|
|
|
29
32
|
let g_totalOutThroughput = 0;
|
|
30
33
|
let g_totalExecTime = 0;
|
|
31
34
|
let g_totalFiles = 0;
|
|
35
|
+
let g_pyVersionInfo = null;
|
|
32
36
|
|
|
33
37
|
function printUsage() {
|
|
34
38
|
console.log(`Usage: node depyo.js [options] <file.pyc|archive.zip> [...]
|
|
@@ -43,6 +47,8 @@ Options:
|
|
|
43
47
|
--skip-source-gen Do not emit .py source (useful with --asm/--dump)
|
|
44
48
|
--skip-path Flatten output paths (write files next to inputs)
|
|
45
49
|
--out Print decompiled source to stdout instead of files
|
|
50
|
+
--marshal Treat input as raw marshalled data (no .pyc header)
|
|
51
|
+
--py-version <x.y> Python bytecode version hint (auto-scan if omitted)
|
|
46
52
|
--basedir <path> Output base directory (default: alongside input)
|
|
47
53
|
--file-ext <ext> Extension for generated source (default: py)
|
|
48
54
|
`);
|
|
@@ -69,6 +75,10 @@ function parseCLIParams() {
|
|
|
69
75
|
g_cliArgs.skipPath = true;
|
|
70
76
|
} else if (cliParam.toLowerCase() == "--out") {
|
|
71
77
|
g_cliArgs.sendToStdout = true;
|
|
78
|
+
} else if (cliParam.toLowerCase() == "--marshal") {
|
|
79
|
+
g_cliArgs.marshal = true;
|
|
80
|
+
} else if (cliParam.toLowerCase() == "--py-version") {
|
|
81
|
+
g_cliArgs.pyVersion = process.argv[++idx];
|
|
72
82
|
} else if (cliParam.toLowerCase() == "--basedir") {
|
|
73
83
|
g_cliArgs.baseDir = process.argv[++idx];
|
|
74
84
|
} else if (cliParam.toLowerCase() == "--file-ext") {
|
|
@@ -84,37 +94,176 @@ function parseCLIParams() {
|
|
|
84
94
|
}
|
|
85
95
|
}
|
|
86
96
|
|
|
97
|
+
function normalizeMarshalOutput(src) {
|
|
98
|
+
return src
|
|
99
|
+
.replace(/\r\n/g, '\n')
|
|
100
|
+
.replace(/[ \t]+$/gm, '')
|
|
101
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
102
|
+
.replace(/#[^\n]*$/gm, '')
|
|
103
|
+
.trim();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function attemptMarshalDecompile(buffer, versionInfo, opts = {}) {
|
|
107
|
+
const prevSilent = g_cliArgs.silent;
|
|
108
|
+
const prevDebug = g_cliArgs.debug;
|
|
109
|
+
if (opts.silent) {
|
|
110
|
+
g_cliArgs.silent = true;
|
|
111
|
+
g_cliArgs.debug = false;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const rdr = new PycReader(buffer, {marshal: true, versionInfo});
|
|
115
|
+
const obj = rdr.ReadObject();
|
|
116
|
+
if (!obj || obj.ClassName !== "Py_CodeObject") {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const opcodes = new versionInfo.opcode(obj);
|
|
120
|
+
const {unknown, total} = PycReader.CountUnknownOpcodes(obj, rdr, opcodes.OpCodeList);
|
|
121
|
+
const unknownRatio = total > 0 ? unknown / total : 1;
|
|
122
|
+
const remaining = rdr.m_rdr.Reader.length - rdr.m_rdr.pc;
|
|
123
|
+
|
|
124
|
+
const genStartTS = process.hrtime.bigint();
|
|
125
|
+
const decompiler = new PycDecompiler(obj);
|
|
126
|
+
const ast = decompiler.decompile();
|
|
127
|
+
const pycResult = ast.codeFragment();
|
|
128
|
+
const pySrc = pycResult.toString();
|
|
129
|
+
const genSecs = Number(process.hrtime.bigint() - genStartTS) / 1000000000;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
reader: rdr,
|
|
133
|
+
obj,
|
|
134
|
+
pySrc,
|
|
135
|
+
genSecs,
|
|
136
|
+
cleanBuild: decompiler.cleanBuild,
|
|
137
|
+
unknown,
|
|
138
|
+
total,
|
|
139
|
+
unknownRatio,
|
|
140
|
+
remaining,
|
|
141
|
+
versionInfo
|
|
142
|
+
};
|
|
143
|
+
} catch (ex) {
|
|
144
|
+
if (opts.debug) {
|
|
145
|
+
console.log(`Marshal decompile failed for ${versionInfo.major}.${versionInfo.minor}: ${ex.message}`);
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
} finally {
|
|
149
|
+
g_cliArgs.silent = prevSilent;
|
|
150
|
+
g_cliArgs.debug = prevDebug;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function selectMarshalCandidate(buffer) {
|
|
155
|
+
const candidates = PycReader.ListSupportedVersions(false);
|
|
156
|
+
const attempts = [];
|
|
157
|
+
const cleanCandidates = [];
|
|
158
|
+
let baselineOutput = null;
|
|
159
|
+
let outputsDiverged = false;
|
|
160
|
+
for (const candidate of candidates) {
|
|
161
|
+
const result = attemptMarshalDecompile(buffer, candidate, {silent: true});
|
|
162
|
+
if (!result) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
attempts.push(result);
|
|
166
|
+
const isWorking = result.cleanBuild && result.unknown === 0 && result.remaining === 0;
|
|
167
|
+
if (isWorking) {
|
|
168
|
+
const normalized = normalizeMarshalOutput(result.pySrc || '');
|
|
169
|
+
cleanCandidates.push({...result, normalized});
|
|
170
|
+
if (baselineOutput === null) {
|
|
171
|
+
baselineOutput = normalized;
|
|
172
|
+
} else if (baselineOutput !== normalized) {
|
|
173
|
+
outputsDiverged = true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!cleanCandidates.length) {
|
|
179
|
+
return attempts.length ? {best: null, attempts, ambiguous: false} : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (outputsDiverged) {
|
|
183
|
+
return {best: null, attempts, ambiguous: true, cleanCandidates};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {best: cleanCandidates[0], attempts, ambiguous: false};
|
|
187
|
+
}
|
|
188
|
+
|
|
87
189
|
function decompilePycObject(data) {
|
|
88
190
|
try
|
|
89
191
|
{
|
|
90
192
|
let filename = null, obj = null;
|
|
91
193
|
let startTS = process.hrtime.bigint();
|
|
92
|
-
let
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
194
|
+
let buffer = data;
|
|
195
|
+
if (!Buffer.isBuffer(buffer)) {
|
|
196
|
+
buffer = fs.readFileSync(data);
|
|
197
|
+
}
|
|
198
|
+
let rdr = null;
|
|
199
|
+
let pySrc = null;
|
|
200
|
+
let genSecs = 0;
|
|
201
|
+
|
|
202
|
+
if (g_cliArgs.marshal) {
|
|
203
|
+
let attemptResult = null;
|
|
204
|
+
if (g_pyVersionInfo) {
|
|
205
|
+
attemptResult = attemptMarshalDecompile(buffer, g_pyVersionInfo);
|
|
206
|
+
} else {
|
|
207
|
+
const scan = selectMarshalCandidate(buffer);
|
|
208
|
+
attemptResult = scan ? scan.best : null;
|
|
209
|
+
if (g_cliArgs.debug && scan?.attempts?.length) {
|
|
210
|
+
console.log("Marshal scan results:");
|
|
211
|
+
for (const attempt of scan.attempts) {
|
|
212
|
+
const info = attempt.versionInfo;
|
|
213
|
+
console.log(` ${info.major}.${info.minor}: clean=${attempt.cleanBuild} unknown=${attempt.unknown}/${attempt.total} remaining=${attempt.remaining}`);
|
|
214
|
+
}
|
|
215
|
+
if (attemptResult) {
|
|
216
|
+
const info = attemptResult.versionInfo;
|
|
217
|
+
console.log(`Selected marshal version: ${info.major}.${info.minor}`);
|
|
102
218
|
}
|
|
103
|
-
return;
|
|
104
219
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
220
|
+
if (!attemptResult && scan?.ambiguous) {
|
|
221
|
+
const versions = scan.cleanCandidates
|
|
222
|
+
? scan.cleanCandidates.map(c => `${c.versionInfo.major}.${c.versionInfo.minor}`).join(', ')
|
|
223
|
+
: 'unknown';
|
|
224
|
+
throw new Error(`Ambiguous marshal version (${versions}). Provide --py-version X.Y to force.`);
|
|
109
225
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!attemptResult) {
|
|
229
|
+
throw new Error("No clean marshal candidate found. Provide --py-version X.Y to force.");
|
|
230
|
+
}
|
|
231
|
+
rdr = attemptResult.reader;
|
|
232
|
+
obj = attemptResult.obj;
|
|
233
|
+
pySrc = attemptResult.pySrc;
|
|
234
|
+
genSecs = attemptResult.genSecs;
|
|
235
|
+
} else {
|
|
236
|
+
rdr = new PycReader(buffer);
|
|
237
|
+
try {
|
|
238
|
+
obj = rdr.ReadObject();
|
|
239
|
+
} catch (ex) {
|
|
240
|
+
if (ex instanceof PycReader.LoadError) {
|
|
241
|
+
// Save the binary file if it not already exists for future manual analysis.
|
|
242
|
+
if (!ex.FileName) {
|
|
243
|
+
if (global.g_cliArgs?.debug) {
|
|
244
|
+
console.log(`LoadError: ${ex.message} at position ${ex.position}`);
|
|
245
|
+
}
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
filename = g_baseDir + ex.FileName;
|
|
249
|
+
let dirPath = Path.dirname(filename);
|
|
250
|
+
if (!g_cliArgs.skipPath && !fs.existsSync(dirPath)) {
|
|
251
|
+
fs.mkdirSync(dirPath, {recursive: true});
|
|
252
|
+
}
|
|
253
|
+
let filenamePyc = filename.substring(0, filename.lastIndexOf('.')) + ".pyc";
|
|
254
|
+
fs.writeFileSync(filenamePyc, rdr.Reader);
|
|
255
|
+
console.log(`Error: ${ex.message}\nFile: ${filenamePyc}\nPosition: ${ex.position}`);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
console.log(`Error: ${ex.message}\nStack:\n${ex.stacktrace}`);
|
|
113
259
|
return;
|
|
114
260
|
}
|
|
115
|
-
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!obj) {
|
|
116
264
|
return;
|
|
117
265
|
}
|
|
266
|
+
filename = g_baseDir + obj.FileName;
|
|
118
267
|
|
|
119
268
|
if (!g_cliArgs.sendToStdout) {
|
|
120
269
|
console.log(`Processing ${filename}...`);
|
|
@@ -139,15 +288,18 @@ function decompilePycObject(data) {
|
|
|
139
288
|
fs.writeFileSync(filenameBase + ".pyasm", PycDisassembler.Disassemble(rdr, obj));
|
|
140
289
|
}
|
|
141
290
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
291
|
+
if (!pySrc) {
|
|
292
|
+
let genStartTS = process.hrtime.bigint();
|
|
293
|
+
let decompiler = new PycDecompiler(obj);
|
|
294
|
+
let ast = decompiler.decompile();
|
|
295
|
+
let pycResult = ast.codeFragment();
|
|
296
|
+
pySrc = pycResult.toString();
|
|
297
|
+
genSecs = Number(process.hrtime.bigint() - genStartTS) / 1000000000;
|
|
298
|
+
}
|
|
147
299
|
if (!pySrc.endsWith("\n")) {
|
|
148
300
|
pySrc += "\n";
|
|
149
301
|
}
|
|
150
|
-
|
|
302
|
+
genSecs = Math.max(genSecs, 0.000000001);
|
|
151
303
|
if (g_cliArgs.sendToStdout) {
|
|
152
304
|
// console.log(`\n\n${filenameBase}.${g_cliArgs.fileExt}\n-------\n${pySrc}`);
|
|
153
305
|
console.log(pySrc);
|
|
@@ -156,9 +308,9 @@ function decompilePycObject(data) {
|
|
|
156
308
|
}
|
|
157
309
|
let secs = parseInt(process.hrtime.bigint() - startTS) / 1000000000;
|
|
158
310
|
g_totalExecTime += secs;
|
|
159
|
-
let inThroughput =
|
|
311
|
+
let inThroughput = buffer.length / genSecs;
|
|
160
312
|
let outThroughput = pySrc.length / genSecs;
|
|
161
|
-
g_totalInThroughput +=
|
|
313
|
+
g_totalInThroughput += buffer.length;
|
|
162
314
|
g_totalOutThroughput += pySrc.length;
|
|
163
315
|
g_totalFiles++;
|
|
164
316
|
if (g_cliArgs.stats) {
|
|
@@ -196,6 +348,17 @@ function DecompileModule(filenames)
|
|
|
196
348
|
}
|
|
197
349
|
|
|
198
350
|
parseCLIParams()
|
|
351
|
+
if (g_cliArgs.pyVersion && !g_cliArgs.marshal) {
|
|
352
|
+
console.log("Error: --py-version requires --marshal (headerless input).");
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
if (g_cliArgs.pyVersion) {
|
|
356
|
+
g_pyVersionInfo = PycReader.ResolveVersionTag(g_cliArgs.pyVersion);
|
|
357
|
+
if (!g_pyVersionInfo) {
|
|
358
|
+
console.log(`Error: unsupported --py-version "${g_cliArgs.pyVersion}". Use format X.Y (e.g., 3.11).`);
|
|
359
|
+
process.exit(1);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
199
362
|
if (g_cliArgs.filenames.length === 0) {
|
|
200
363
|
printUsage();
|
|
201
364
|
process.exit(1);
|
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,129 @@ 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 CountUnknownOpcodes(codeObject, reader, opCodeList) {
|
|
284
|
+
const code = codeObject?.Code?.Value;
|
|
285
|
+
if (!code || !opCodeList) {
|
|
286
|
+
return {unknown: Number.MAX_SAFE_INTEGER, total: 0};
|
|
287
|
+
}
|
|
288
|
+
const wordSize = reader.getInstructionWordSize();
|
|
289
|
+
let unknown = 0;
|
|
290
|
+
let total = 0;
|
|
291
|
+
if (wordSize === 2) {
|
|
292
|
+
for (let offset = 0; offset < code.length; offset += 2) {
|
|
293
|
+
total++;
|
|
294
|
+
if (!opCodeList[code[offset]]) {
|
|
295
|
+
unknown++;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
let offset = 0;
|
|
300
|
+
while (offset < code.length) {
|
|
301
|
+
total++;
|
|
302
|
+
const opcode = code[offset];
|
|
303
|
+
const entry = opCodeList[opcode];
|
|
304
|
+
if (!entry) {
|
|
305
|
+
unknown++;
|
|
306
|
+
offset += 1;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
offset += entry.HasArgument ? 3 : 1;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return {unknown, total};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
static TryParseMarshal(buffer, versionInfo) {
|
|
316
|
+
try {
|
|
317
|
+
const reader = new PycReader(buffer, {marshal: true, versionInfo, silent: true});
|
|
318
|
+
const obj = reader.ReadObject();
|
|
319
|
+
if (!obj || obj.ClassName !== "Py_CodeObject") {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
const remaining = reader.m_rdr.Reader.length - reader.m_rdr.pc;
|
|
323
|
+
if (remaining < 0) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const prevDebug = global.g_cliArgs?.debug;
|
|
328
|
+
if (global.g_cliArgs) {
|
|
329
|
+
global.g_cliArgs.debug = false;
|
|
330
|
+
}
|
|
331
|
+
let opcodes = null;
|
|
332
|
+
try {
|
|
333
|
+
opcodes = new versionInfo.opcode(obj);
|
|
334
|
+
} finally {
|
|
335
|
+
if (global.g_cliArgs) {
|
|
336
|
+
global.g_cliArgs.debug = prevDebug;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const {unknown, total} = PycReader.CountUnknownOpcodes(obj, reader, opcodes?.OpCodeList);
|
|
341
|
+
const unknownRatio = total > 0 ? unknown / total : 1;
|
|
342
|
+
|
|
343
|
+
let score = 0;
|
|
344
|
+
if (remaining === 0) {
|
|
345
|
+
score += 3;
|
|
346
|
+
}
|
|
347
|
+
if (unknown === 0) {
|
|
348
|
+
score += 2;
|
|
349
|
+
}
|
|
350
|
+
if (obj.Code?.Value && Buffer.isBuffer(obj.Code.Value)) {
|
|
351
|
+
score += 1;
|
|
352
|
+
}
|
|
353
|
+
if (obj.Consts?.ClassName === "Py_Tuple") {
|
|
354
|
+
score += 1;
|
|
355
|
+
}
|
|
356
|
+
if (obj.Names?.ClassName === "Py_Tuple") {
|
|
357
|
+
score += 1;
|
|
358
|
+
}
|
|
359
|
+
return {score, remaining, unknown, unknownRatio};
|
|
360
|
+
} catch (ex) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
193
365
|
ReadObject() {
|
|
194
366
|
try {
|
|
195
367
|
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
|
|