xasm 1.2.1__py37-none-any.whl

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.
xasm/pyc_convert.py ADDED
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env python
2
+ """Convert Python Bytecode from one version to another for
3
+ some limited set of Python bytecode versions
4
+ """
5
+ import os
6
+ import os.path as osp
7
+ from copy import copy
8
+ from tempfile import NamedTemporaryFile
9
+
10
+ import click
11
+ import xdis
12
+ from xdis import disassemble_file, load_module, magic2int, write_bytecode_file
13
+ from xdis.magics import magics
14
+ from xdis.opcodes import opcode_27, opcode_33
15
+
16
+ from xasm.assemble import (
17
+ Assembler,
18
+ Instruction,
19
+ asm_file,
20
+ create_code,
21
+ decode_lineno_tab_old,
22
+ )
23
+ from xasm.version import __version__
24
+ from xasm.write_pyc import write_pycfile
25
+
26
+
27
+ def add_credit(asm, src_version, dest_version) -> None:
28
+ stamp = "Converted from Python %s to %s by %s version %s" % (
29
+ src_version,
30
+ dest_version,
31
+ "pyc-convert",
32
+ __version__,
33
+ )
34
+ asm.codes[-1].co_consts = list(asm.codes[-1].co_consts).append(stamp)
35
+ return
36
+
37
+
38
+ def copy_magic_into_pyc(input_pyc, output_pyc, src_version, dest_version) -> None:
39
+ """Bytecodes are the same except the magic number, so just change
40
+ that"""
41
+ (version, timestamp, magic_int, co, is_pypy, source_size) = load_module(input_pyc)
42
+ assert version == float(
43
+ src_version
44
+ ), f"Need Python {src_version} bytecode; got bytecode for version {version}"
45
+ magic_int = magic2int(magics.magics[dest_version])
46
+ write_bytecode_file(output_pyc, co, magic_int)
47
+ print(f"Wrote {output_pyc}")
48
+ return
49
+
50
+
51
+ def xlate26_27(inst) -> None:
52
+ """Between 2.6 and 2.7 opcode values changed
53
+ Adjust for the differences by using the opcode name
54
+ """
55
+ inst.opcode = opcode_27.opmap[inst.opname]
56
+
57
+
58
+ def conversion_to_version(conversion_type, is_dest=False):
59
+ if is_dest:
60
+ return conversion_type[-2] + "." + conversion_type[-1]
61
+ else:
62
+ return conversion_type[0] + "." + conversion_type[1]
63
+
64
+
65
+ def transform_26_27(inst, new_inst, i, n, offset, instructions, new_asm):
66
+ """Change JUMP_IF_FALSE and JUMP_IF_TRUE to
67
+ POP_JUMP_IF_FALSE and POP_JUMP_IF_TRUE"""
68
+ if inst.opname in ("JUMP_IF_FALSE", "JUMP_IF_TRUE"):
69
+ i += 1
70
+ assert i < n
71
+ assert instructions[i].opname == "POP_TOP"
72
+ new_inst.offset = offset
73
+ new_inst.opname = (
74
+ "POP_JUMP_IF_FALSE"
75
+ if inst.opname == "JUMP_IF_FALSE"
76
+ else "POP_JUMP_IF_TRUE"
77
+ )
78
+ new_asm.backpatch[-1].remove(inst)
79
+ new_inst.arg = "L%d" % (inst.offset + inst.arg + 3)
80
+ new_asm.backpatch[-1].add(new_inst)
81
+ else:
82
+ xlate26_27(new_inst)
83
+ return xdis.op_size(new_inst.opcode, opcode_27)
84
+
85
+
86
+ def transform_32_33(inst, new_inst, i, n, offset, instructions, new_asm):
87
+ """MAKE_FUNCTION adds another const. probably MAKE_CLASS as well"""
88
+ add_size = xdis.op_size(new_inst.opcode, opcode_33)
89
+ if inst.opname in ("MAKE_FUNCTION", "MAKE_CLOSURE"):
90
+ # Previous instruction should be a load const which
91
+ # contains the name of the function to call
92
+ prev_inst = instructions[i - 1]
93
+ assert prev_inst.opname == "LOAD_CONST"
94
+ assert isinstance(prev_inst.arg, int)
95
+
96
+ # Add the function name as an additional LOAD_CONST
97
+ load_fn_const = Instruction()
98
+ load_fn_const.opname = "LOAD_CONST"
99
+ load_fn_const.opcode = opcode_33.opmap["LOAD_CONST"]
100
+ load_fn_const.line_no = None
101
+ prev_const = new_asm.code.co_consts[prev_inst.arg]
102
+ if hasattr(prev_const, "co_name"):
103
+ fn_name = new_asm.code.co_consts[prev_inst.arg].co_name
104
+ else:
105
+ fn_name = "what-is-up"
106
+ const_index = len(new_asm.code.co_consts)
107
+ new_asm.code.co_consts = list(new_asm.code.co_consts)
108
+ new_asm.code.co_consts.append(fn_name)
109
+ load_fn_const.arg = const_index
110
+ load_fn_const.offset = offset
111
+ load_fn_const.starts_line = False
112
+ load_fn_const.is_jump_target = False
113
+ new_asm.code.instructions.append(load_fn_const)
114
+ load_const_size = xdis.op_size(load_fn_const.opcode, opcode_33)
115
+ add_size += load_const_size
116
+ new_inst.offset = offset + add_size
117
+ pass
118
+ return add_size
119
+
120
+
121
+ def transform_33_32(inst, new_inst, i, n, offset, instructions, new_asm):
122
+ """MAKE_FUNCTION, and MAKE_CLOSURE have an additional LOAD_CONST of a name
123
+ that are not in Python 3.2. Remove these.
124
+ """
125
+ add_size = xdis.op_size(new_inst.opcode, opcode_33)
126
+ if inst.opname in ("MAKE_FUNCTION", "MAKE_CLOSURE"):
127
+ # Previous instruction should be a load const which
128
+ # contains the name of the function to call
129
+ prev_inst = instructions[i - 1]
130
+ assert prev_inst.opname == "LOAD_CONST"
131
+ assert isinstance(prev_inst.arg, int)
132
+ assert len(instructions) > 2
133
+ assert len(instructions) > 2
134
+ prev_inst2 = instructions[i - 2]
135
+ assert prev_inst2.opname == "LOAD_CONST"
136
+ assert isinstance(prev_inst2.arg, int)
137
+
138
+ # Remove the function name as an additional LOAD_CONST
139
+ prev2_const = new_asm.code.co_consts[prev_inst.arg]
140
+ assert hasattr(prev2_const, "co_name")
141
+ new_asm.code.instructions = new_asm.code.instructions[:-1]
142
+ load_const_size = xdis.op_size(prev_inst.opcode, opcode_33)
143
+ add_size -= load_const_size
144
+ new_inst.offset = offset - add_size
145
+ return -load_const_size
146
+ return 0
147
+
148
+
149
+ def transform_asm(
150
+ asm: Assembler | None, conversion_type, src_version, dest_version
151
+ ) -> Assembler:
152
+ new_asm = Assembler(dest_version, is_pypy=False)
153
+ for field in "code size".split():
154
+ setattr(new_asm, field, copy(getattr(asm, field)))
155
+
156
+ if conversion_type == "26-27":
157
+ transform_fn = transform_26_27
158
+ elif conversion_type == "32-33":
159
+ transform_fn = transform_32_33
160
+ elif conversion_type == "33-32":
161
+ transform_fn = transform_33_32
162
+ else:
163
+ raise RuntimeError(f"Don't know how to convert {conversion_type} ")
164
+ for j, code in enumerate(asm.code_list):
165
+ offset2label = {v: k for k, v in asm.label[j].items()}
166
+ new_asm.backpatch.append(copy(asm.backpatch[j]))
167
+ new_asm.label.append(copy(asm.label[j]))
168
+ new_asm.codes.append(copy(code))
169
+ new_asm.code.co_lnotab = decode_lineno_tab_old(
170
+ code.co_lnotab, code.co_firstlineno
171
+ )
172
+ instructions = asm.codes[j].instructions
173
+ new_asm.code.instructions = []
174
+ i, offset, n = 0, 0, len(instructions)
175
+ while i < n:
176
+ inst = instructions[i]
177
+ new_inst = copy(inst)
178
+ inst_size = transform_fn(
179
+ inst, new_inst, i, offset, n, instructions, new_asm
180
+ )
181
+ if inst.offset in offset2label:
182
+ new_asm.label[-1][offset2label[inst.offset]] = offset
183
+ pass
184
+ offset += inst_size
185
+ new_asm.code.instructions.append(new_inst)
186
+ i += 1
187
+ pass
188
+
189
+ co, is_valid = create_code(new_asm, new_asm.label[-1], new_asm.backpatch[-1])
190
+ new_asm.code_list.append(co)
191
+ new_asm.code_list.reverse()
192
+ new_asm.status = "finished" if is_valid else "invalid"
193
+ return new_asm
194
+
195
+
196
+ UPWARD_COMPATIBLE = tuple("20-21 21-22 23-24 24-23".split())
197
+
198
+
199
+ @click.command()
200
+ @click.option(
201
+ "--conversion-type",
202
+ "-t",
203
+ type=click.Choice(
204
+ [
205
+ "20-21",
206
+ "21-22",
207
+ "23-24",
208
+ "24-23",
209
+ "24-25",
210
+ "25-26",
211
+ "26-27",
212
+ "32-33",
213
+ "33-32",
214
+ ]
215
+ ),
216
+ help="specify conversion from/to bytecode",
217
+ default="26-27",
218
+ )
219
+ @click.argument("input_pyc", type=click.Path(writable=True), nargs=1)
220
+ @click.argument(
221
+ "output_pyc", type=click.Path(writable=True), required=False, nargs=1, default=None
222
+ )
223
+ def main(conversion_type, input_pyc, output_pyc) -> None:
224
+ """Convert Python bytecode from one version to another.
225
+
226
+ INPUT_PYC contains the input bytecode path name
227
+ OUTPUT_PYC contains the output bytecode path name if supplied
228
+ The --conversion type option specifies what conversion to do.
229
+
230
+ Note: there are a very limited set of conversions currently supported.
231
+ Help out and write more!"""
232
+
233
+ shortname = osp.basename(input_pyc)
234
+ if shortname.endswith(".pyc"):
235
+ shortname = shortname[:-4]
236
+ src_version = conversion_to_version(conversion_type, is_dest=False)
237
+ dest_version = conversion_to_version(conversion_type, is_dest=True)
238
+ if output_pyc is None:
239
+ output_pyc = f"{shortname}-{dest_version}.pyc"
240
+
241
+ if conversion_type in UPWARD_COMPATIBLE:
242
+ copy_magic_into_pyc(input_pyc, output_pyc, src_version, dest_version)
243
+ return
244
+ temp_asm = NamedTemporaryFile("w", suffix=".pyasm", prefix=shortname, delete=False)
245
+ (filename, co, version, timestamp, magic_int) = disassemble_file(
246
+ input_pyc, temp_asm, asm_format=True
247
+ )
248
+ temp_asm.close()
249
+ assert version == float(
250
+ src_version
251
+ ), f"Need Python {src_version} bytecode; got bytecode for version {version}"
252
+ asm = asm_file(temp_asm.name)
253
+ new_asm = transform_asm(asm, conversion_type, src_version, dest_version)
254
+ os.unlink(temp_asm.name)
255
+ write_pycfile(output_pyc, new_asm)
256
+
257
+
258
+ if __name__ == "__main__":
259
+ main()
xasm/version.py ADDED
@@ -0,0 +1,8 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # This file is suitable for sourcing inside POSIX shell as
4
+ # well as importing into Python. That's why there is no
5
+ # space around "=" below.
6
+
7
+ # fmt: off
8
+ __version__="1.2.1" # noqa
xasm/write_pyc.py ADDED
@@ -0,0 +1,48 @@
1
+ import time
2
+ from struct import pack
3
+
4
+ import xdis
5
+ from xdis import magic2int
6
+ from xdis.magics import magics
7
+ from xdis.marsh import dumps
8
+ from xdis.version_info import PYTHON3, version_tuple_to_str
9
+
10
+
11
+ def write_pycfile(fp, code_list, timestamp=None, version_triple=xdis.PYTHON_VERSION_TRIPLE) -> int:
12
+
13
+ rc = 0
14
+ version_str = version_tuple_to_str(version_triple, end=2)
15
+ magic_bytes = magics[version_str]
16
+ magic_int = magic2int(magic_bytes)
17
+ fp.write(magic_bytes)
18
+
19
+ if timestamp is None:
20
+ timestamp = int(time.time())
21
+ write_source_size = version_triple >= (3, 3)
22
+ if version_triple >= (3, 7):
23
+ if magic_int == 3393:
24
+ fp.write(pack("I", timestamp))
25
+ fp.write(pack("I", 0))
26
+ else:
27
+ # PEP 552. https://www.python.org/dev/peps/pep-0552/
28
+ # 0 in the lowest-order bit means used old-style timestamps
29
+ fp.write(pack("<I", 0))
30
+ fp.write(pack("<I", timestamp))
31
+ else:
32
+ fp.write(pack("<I", timestamp))
33
+
34
+ if write_source_size:
35
+ fp.write(pack("<I", 0)) # size mod 2**32
36
+
37
+ for co in code_list:
38
+ try:
39
+ co_obj = dumps(co, python_version=version_triple)
40
+ if PYTHON3 and version_triple < (3, 0):
41
+ co_obj = str.encode(co_obj)
42
+ pass
43
+
44
+ fp.write(co_obj)
45
+ except Exception as e:
46
+ print(f"error dumping {co}: {e}; ignoring")
47
+ rc = 1
48
+ return rc
xasm/xasm_cli.py ADDED
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env python
2
+ import os
3
+ import sys
4
+ from typing import List
5
+
6
+ import click
7
+ import xdis
8
+ from xdis.version_info import version_tuple_to_str
9
+
10
+ from xasm.assemble import asm_file
11
+ from xasm.write_pyc import write_pycfile
12
+
13
+
14
+ @click.command()
15
+ @click.option("--pyc-file", default=None)
16
+ @click.argument("asm-path", type=click.Path(exists=True, readable=True), required=True)
17
+ def main(pyc_file: List[str], asm_path):
18
+ """
19
+ Create Python bytecode from a Python assembly file.
20
+
21
+ ASM_PATH gives the input Python assembly file. We suggest ending the
22
+ file in .pyc
23
+
24
+ If --pyc-file is given, that indicates the path to write the
25
+ Python bytecode. The path should end in '.pyc'.
26
+
27
+ See https://github.com/rocky/python-xasm/blob/master/HOW-TO-USE.rst
28
+ for how to write a Python assembler file.
29
+ """
30
+ if os.stat(asm_path).st_size == 0:
31
+ print(f"Size of assembly file {asm_path} is zero")
32
+ sys.exit(1)
33
+ asm = asm_file(asm_path)
34
+
35
+ if not pyc_file:
36
+ if asm_path.endswith(".pyasm"):
37
+ pyc_file = asm_path[: -len(".pyasm")] + ".pyc"
38
+ elif not pyc_file and asm_path.endswith(".xasm"):
39
+ pyc_file = asm_path[: -len(".xasm")] + ".pyc"
40
+
41
+ if xdis.PYTHON3:
42
+ file_mode = "wb"
43
+ else:
44
+ file_mode = "w"
45
+
46
+ with open(pyc_file, file_mode) as fp:
47
+ rc = write_pycfile(fp, asm.code_list, asm.timestamp, asm.python_version)
48
+ size = fp.tell()
49
+ print(
50
+ f"""Wrote Python {version_tuple_to_str(asm.python_version)} bytecode file "{pyc_file}"; {size} bytes."""
51
+ )
52
+ if size <= 16:
53
+ print("Warning: bytecode file is too small to be usable.")
54
+ rc = 2
55
+ if rc != 0:
56
+ print(f"Exiting with return code {rc}")
57
+ sys.exit(rc)
58
+
59
+
60
+ if __name__ == "__main__":
61
+ main(sys.argv[1:])