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
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
const AST = require('../ast/ast_node');
|
|
2
|
+
|
|
3
|
+
function handleBreakLoop() {
|
|
4
|
+
let keywordNode = new AST.ASTKeyword(AST.ASTKeyword.Word.Break);
|
|
5
|
+
keywordNode.line = this.code.Current.LineNo;
|
|
6
|
+
this.curBlock.append(keywordNode);
|
|
7
|
+
|
|
8
|
+
// Mark code as unreachable until the end of CURRENT BLOCK (not entire loop)
|
|
9
|
+
// This allows alternative branches (elif/else) to be processed correctly
|
|
10
|
+
//
|
|
11
|
+
// WRONG approach: unreachableUntil = loopBlock.end (too broad, skips elif!)
|
|
12
|
+
// RIGHT approach: unreachableUntil = curBlock.end (only this branch)
|
|
13
|
+
if (this.curBlock.end > this.code.Current.Offset) {
|
|
14
|
+
this.unreachableUntil = this.curBlock.end;
|
|
15
|
+
|
|
16
|
+
if (global.g_cliArgs?.debug) {
|
|
17
|
+
console.log(`BREAK at offset ${this.code.Current.Offset}: curBlock=${this.curBlock.type_str} (${this.curBlock.start}-${this.curBlock.end})`);
|
|
18
|
+
console.log(` Block stack: ${this.blocks.map((b,i) => `[${i}]${b.type_str}(${b.start}-${b.end})`).join(', ')}`);
|
|
19
|
+
console.log(` Marking unreachable until ${this.unreachableUntil}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function handleSetupLoopA() {
|
|
25
|
+
if (global.g_cliArgs?.debug) {
|
|
26
|
+
console.log(`[handleSetupLoopA] Creating while at offset ${this.code.Current.Offset}, JumpTarget=${this.code.Current.JumpTarget}`);
|
|
27
|
+
console.log(` Data stack size: ${this.dataStack.length}`);
|
|
28
|
+
if (this.dataStack.length > 0) {
|
|
29
|
+
let top = this.dataStack.top();
|
|
30
|
+
console.log(` Stack top: ${top?.constructor?.name} = ${top?.codeFragment ? top.codeFragment() : top}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let nextBlock = new AST.ASTCondBlock(AST.ASTBlock.BlockType.While, this.code.Current.Offset, this.code.Current.JumpTarget, null, false);
|
|
35
|
+
nextBlock.line = this.code.Current.LineNo;
|
|
36
|
+
this.blocks.push(nextBlock);
|
|
37
|
+
this.curBlock = this.blocks.top();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function handleContinueLoopA() {
|
|
41
|
+
let node = new AST.ASTKeyword (AST.ASTKeyword.Word.Continue);
|
|
42
|
+
node.line = this.code.Current.LineNo;
|
|
43
|
+
this.curBlock.nodes.push(node);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function handleForIterA() {
|
|
47
|
+
handleInstrumentedForIterA.call(this);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function handleInstrumentedForIterA() {
|
|
51
|
+
let iter = this.dataStack.pop(); // Iterable
|
|
52
|
+
/* Pop it? Don't pop it? */
|
|
53
|
+
|
|
54
|
+
let start = this.code.Current.Offset;
|
|
55
|
+
let end = 0;
|
|
56
|
+
let line = this.code.Current.LineNo;
|
|
57
|
+
let comprehension = false;
|
|
58
|
+
const specialized = this.object.Reader.versionCompare(3, 13) >= 0;
|
|
59
|
+
const sentinel = specialized ? new AST.ASTNone() : null;
|
|
60
|
+
|
|
61
|
+
// before 3.8, there is a SETUP_LOOP instruction with block start and end position,
|
|
62
|
+
// the this.code.Current.Argument is usually a jump to a POP_BLOCK instruction
|
|
63
|
+
// after 3.8, block extent has to be inferred implicitly; the this.code.Current.Argument is a jump to a position after the for block
|
|
64
|
+
if (this.object.Reader.versionCompare(3, 8) >= 0) {
|
|
65
|
+
end = this.code.Current.Argument;
|
|
66
|
+
if (this.object.Reader.versionCompare(3, 10) >= 0)
|
|
67
|
+
end *= 2; // // BPO-27129
|
|
68
|
+
end += this.code.Next?.Offset;
|
|
69
|
+
[end] = this.code.FindEndOfBlock(end);
|
|
70
|
+
const objName = this.object?.Name;
|
|
71
|
+
comprehension = this.code.Current.Name == "<listcomp>" ||
|
|
72
|
+
objName == "<listcomp>" ||
|
|
73
|
+
objName == "<setcomp>" ||
|
|
74
|
+
objName == "<dictcomp>";
|
|
75
|
+
if (!comprehension) {
|
|
76
|
+
const container = this.dataStack.top();
|
|
77
|
+
if ((container instanceof AST.ASTList || container instanceof AST.ASTSet || container instanceof AST.ASTMap) &&
|
|
78
|
+
(container.values?.length === 0)) {
|
|
79
|
+
comprehension = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
if ((this.dataStack.top() instanceof AST.ASTSet ||
|
|
84
|
+
this.dataStack.top() instanceof AST.ASTList ||
|
|
85
|
+
this.dataStack.top() instanceof AST.ASTMap)
|
|
86
|
+
&& this.dataStack.top().values.length == 0) {
|
|
87
|
+
end = this.code.Current.JumpTarget;
|
|
88
|
+
comprehension = true;
|
|
89
|
+
} else {
|
|
90
|
+
let top = this.blocks.top();
|
|
91
|
+
start = top.start;
|
|
92
|
+
end = top.end; // block end position from SETUP_LOOP
|
|
93
|
+
line = top.line;
|
|
94
|
+
|
|
95
|
+
if (top.blockType == AST.ASTBlock.BlockType.While) {
|
|
96
|
+
this.blocks.pop();
|
|
97
|
+
} else {
|
|
98
|
+
comprehension = true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let forblk = new AST.ASTIterBlock(AST.ASTBlock.BlockType.For, start, end, iter);
|
|
104
|
+
forblk.line = line;
|
|
105
|
+
forblk.comprehension = comprehension;
|
|
106
|
+
|
|
107
|
+
if (global.g_cliArgs?.debug) {
|
|
108
|
+
console.log(`[FOR_ITER] Created for block: start=${start}, end=${end}, comprehension=${comprehension}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.blocks.push(forblk);
|
|
112
|
+
this.curBlock = this.blocks.top();
|
|
113
|
+
|
|
114
|
+
// 3.13+ FOR_ITER pushes a sentinel used by END_FOR; keep stack balanced.
|
|
115
|
+
this.dataStack.push(sentinel);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function handleForLoopA() {
|
|
119
|
+
let curidx = this.dataStack.pop(); // Current index
|
|
120
|
+
let iter = this.dataStack.pop(); // Iterable
|
|
121
|
+
|
|
122
|
+
let comprehension = false;
|
|
123
|
+
let top = this.blocks.top();
|
|
124
|
+
|
|
125
|
+
if (top.blockType == AST.ASTBlock.BlockType.While) {
|
|
126
|
+
this.blocks.pop();
|
|
127
|
+
} else {
|
|
128
|
+
comprehension = true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let forblk = new AST.ASTIterBlock(AST.ASTBlock.BlockType.For, this.code.Current.Offset, top.end, iter);
|
|
132
|
+
forblk.line = this.code.Current.LineNo;
|
|
133
|
+
forblk.comprehension = comprehension;
|
|
134
|
+
this.blocks.push(forblk);
|
|
135
|
+
this.curBlock = this.blocks.top();
|
|
136
|
+
|
|
137
|
+
this.dataStack.push(iter);
|
|
138
|
+
this.dataStack.push(curidx);
|
|
139
|
+
this.dataStack.push(null);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function handleGetAiter() {
|
|
143
|
+
// Logic similar to FOR_ITER_A
|
|
144
|
+
let iter = this.dataStack.pop(); // Iterable
|
|
145
|
+
|
|
146
|
+
let start = this.code.Current.Offset;
|
|
147
|
+
let end = 0;
|
|
148
|
+
let line = this.code.Current.LineNo;
|
|
149
|
+
let comprehension = false;
|
|
150
|
+
|
|
151
|
+
let top = this.blocks.top();
|
|
152
|
+
|
|
153
|
+
// Python 3.8+ removed SETUP_LOOP, so we need to infer the block end
|
|
154
|
+
if (this.object.Reader.versionCompare(3, 8) >= 0 || top.blockType != AST.ASTBlock.BlockType.While) {
|
|
155
|
+
// Python 3.11+: Check exception table first for END_ASYNC_FOR location
|
|
156
|
+
if (this.object.Reader.versionCompare(3, 11) >= 0 && this.object.ExceptionTable && this.object.ExceptionTable.length > 0) {
|
|
157
|
+
if (global.g_cliArgs?.debug) {
|
|
158
|
+
console.log(`[GET_AITER] Python 3.11+ with exception table (${this.object.ExceptionTable.length} entries)`);
|
|
159
|
+
this.object.ExceptionTable.forEach((entry, i) => {
|
|
160
|
+
console.log(` Entry ${i}: start=${entry.start}, end=${entry.end}, target=${entry.target}, depth=${entry.depth}`);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Find exception handler that covers the async for loop
|
|
165
|
+
// The handler's target offset should point to END_ASYNC_FOR
|
|
166
|
+
// Note: exception table start may be GET_ANEXT (after GET_AITER)
|
|
167
|
+
for (const entry of this.object.ExceptionTable) {
|
|
168
|
+
// Check if this entry covers range starting at/near GET_AITER
|
|
169
|
+
// GET_AITER is at 'start', exception table may start at next instruction
|
|
170
|
+
if (start >= entry.start - 4 && start <= entry.start + 4) {
|
|
171
|
+
// The target is the exception handler (END_ASYNC_FOR)
|
|
172
|
+
end = entry.target + 2; // END_ASYNC_FOR is 2 bytes, end is after it
|
|
173
|
+
|
|
174
|
+
if (global.g_cliArgs?.debug) {
|
|
175
|
+
console.log(` ✓ Found END_ASYNC_FOR via exception table: target=${entry.target}, end=${end}`);
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// If not found via exception table, fall back to search
|
|
183
|
+
if (end == 0) {
|
|
184
|
+
// Find END_ASYNC_FOR to determine block end
|
|
185
|
+
// Scan forward by offset (not by instruction index)
|
|
186
|
+
let searchOffset = this.code.Next.Offset;
|
|
187
|
+
let searchLimit = 200;
|
|
188
|
+
let searchCount = 0;
|
|
189
|
+
|
|
190
|
+
if (global.g_cliArgs?.debug) {
|
|
191
|
+
console.log(`[GET_AITER] Fallback: searching for END_ASYNC_FOR from offset ${searchOffset}`);
|
|
192
|
+
console.log(` Last offset: ${this.code.LastOffset}, instructions: ${this.code.Instructions.length}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
while (searchCount < searchLimit) {
|
|
196
|
+
let instr = this.code.PeekInstructionAtOffset(searchOffset);
|
|
197
|
+
if (!instr) {
|
|
198
|
+
if (global.g_cliArgs?.debug) {
|
|
199
|
+
console.log(` No instruction at offset ${searchOffset}, stopping search (searched ${searchCount} bytes)`);
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (global.g_cliArgs?.debug && searchCount < 30) {
|
|
205
|
+
console.log(` [${searchCount}] offset=${instr.Offset}, opcode=${instr.InstructionName}, id=${instr.OpCodeID}, size=${instr.Size}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (instr.OpCodeID == this.OpCodes.END_ASYNC_FOR) {
|
|
209
|
+
let instrSize = instr.Size || 2;
|
|
210
|
+
end = instr.Offset + instrSize;
|
|
211
|
+
if (global.g_cliArgs?.debug) {
|
|
212
|
+
console.log(` ✓ Found END_ASYNC_FOR at offset ${instr.Offset}, size=${instrSize}, end=${end}`);
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Python 3.6+ uses 2-byte (word-aligned) instructions
|
|
218
|
+
let instrSize = instr.Size || 2;
|
|
219
|
+
searchOffset += instrSize;
|
|
220
|
+
searchCount += instrSize;
|
|
221
|
+
|
|
222
|
+
if (global.g_cliArgs?.debug && searchCount < 10) {
|
|
223
|
+
console.log(` Next search: offset=${searchOffset}, count=${searchCount}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (end == 0) {
|
|
228
|
+
if (global.g_cliArgs?.debug) {
|
|
229
|
+
console.error(`Could not find END_ASYNC_FOR for GET_AITER at offset ${start}`);
|
|
230
|
+
}
|
|
231
|
+
end = this.code.LastOffset + 2; // Conservative fallback: run to end of code
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Check if this is an async comprehension (generator expression)
|
|
236
|
+
comprehension = this.code.Current.Name &&
|
|
237
|
+
(this.code.Current.Name.includes("comp>") ||
|
|
238
|
+
this.code.Current.Name.includes("genexpr>"));
|
|
239
|
+
|
|
240
|
+
let forblk = new AST.ASTIterBlock(AST.ASTBlock.BlockType.AsyncFor, start, end, iter);
|
|
241
|
+
forblk.line = line;
|
|
242
|
+
forblk.comprehension = comprehension;
|
|
243
|
+
this.blocks.push(forblk);
|
|
244
|
+
this.curBlock = this.blocks.top();
|
|
245
|
+
this.dataStack.push(null);
|
|
246
|
+
} else if (top.blockType == AST.ASTBlock.BlockType.While) {
|
|
247
|
+
// Python 3.7 and earlier with SETUP_LOOP: SETUP_LOOP creates While block first
|
|
248
|
+
if (global.g_cliArgs?.debug) {
|
|
249
|
+
console.log(`[GET_AITER] Converting While to AsyncFor at offset ${start}, end=${top.end}`);
|
|
250
|
+
}
|
|
251
|
+
this.blocks.pop();
|
|
252
|
+
let forblk = new AST.ASTIterBlock(AST.ASTBlock.BlockType.AsyncFor, top.start, top.end, iter);
|
|
253
|
+
forblk.line = line;
|
|
254
|
+
this.blocks.push(forblk);
|
|
255
|
+
this.curBlock = this.blocks.top();
|
|
256
|
+
this.dataStack.push(null);
|
|
257
|
+
} else {
|
|
258
|
+
console.error(`Unexpected block type for GET_AITER: ${top.type_str} at offset ${start}\n`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function handleGetAnext() {
|
|
263
|
+
let iter = this.dataStack.top();
|
|
264
|
+
let callNode = new AST.ASTCall(new AST.ASTName('await'), [new AST.ASTBinary(iter, new AST.ASTName('__anext__'), AST.ASTBinary.BinOp.Attr)], []);
|
|
265
|
+
callNode.line = this.code.Current.LineNo;
|
|
266
|
+
this.dataStack.push(callNode);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function handleGetIter() {
|
|
270
|
+
handleGetYieldFromIter.call(this);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function handleGetYieldFromIter() {
|
|
274
|
+
/* We just entirely ignore this */
|
|
275
|
+
if (this.code.Next?.OpCodeID == this.OpCodes.CALL_FUNCTION_A) {
|
|
276
|
+
this.dataStack.push(new AST.ASTIteratorValue(this.dataStack.pop()));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function handleEndFor() {
|
|
281
|
+
// Python 3.13+ END_FOR opcode
|
|
282
|
+
// Cleans up after for loop iteration
|
|
283
|
+
// In Python 3.13, FOR_ITER pushes sentinel, END_FOR pops it
|
|
284
|
+
if (global.g_cliArgs?.debug) {
|
|
285
|
+
console.log(`[END_FOR] at offset ${this.code.Current.Offset}`);
|
|
286
|
+
}
|
|
287
|
+
if (this.object.Reader.versionCompare(3, 13) >= 0 && this.dataStack.length > 0) {
|
|
288
|
+
this.dataStack.pop();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function handleInstrumentedEndForA() {
|
|
293
|
+
// Instrumented variant mirrors END_FOR behavior.
|
|
294
|
+
handleEndFor.call(this);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function handleInstrumentedPopIterA() {
|
|
298
|
+
// Instrumentation helper; no stack effect for decompilation.
|
|
299
|
+
if (global.g_cliArgs?.debug) {
|
|
300
|
+
console.log(`[INSTRUMENTED_POP_ITER] at offset ${this.code.Current.Offset}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
module.exports = {
|
|
305
|
+
handleBreakLoop,
|
|
306
|
+
handleContinueLoopA,
|
|
307
|
+
handleEndFor,
|
|
308
|
+
handleInstrumentedEndForA,
|
|
309
|
+
handleInstrumentedPopIterA,
|
|
310
|
+
handleForIterA,
|
|
311
|
+
handleInstrumentedForIterA,
|
|
312
|
+
handleForLoopA,
|
|
313
|
+
handleGetAiter,
|
|
314
|
+
handleGetAnext,
|
|
315
|
+
handleGetIter,
|
|
316
|
+
handleGetYieldFromIter,
|
|
317
|
+
handleSetupLoopA,
|
|
318
|
+
};
|