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 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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "depyo",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Python bytecode decompiler (Python 1.0–3.14) implemented in Node.js",
5
5
  "bin": {
6
6
  "depyo": "./depyo.js"