-
Notifications
You must be signed in to change notification settings - Fork 58
/
unpy2exe.py
179 lines (138 loc) · 5.02 KB
/
unpy2exe.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
import imp
import logging
import marshal
import ntpath
import os
import struct
import sys
import time
import pefile
import six
IGNORE = [
'<install zipextimporter>.pyc', # zip importer added by py2exe
'<bootstrap2>.pyc', # bootstrap added by py2exe
'boot_common.py.pyc', # boot_common added by py2exe
]
def __build_magic(magic):
"""Build Python magic number for pyc header."""
return struct.pack(b'Hcc', magic, b'\r', b'\n')
PYTHON_MAGIC_WORDS = {
# version magic numbers (see Python/Lib/importlib/_bootstrap_external.py)
'1.5': __build_magic(20121),
'1.6': __build_magic(50428),
'2.0': __build_magic(50823),
'2.1': __build_magic(60202),
'2.2': __build_magic(60717),
'2.3': __build_magic(62011),
'2.4': __build_magic(62061),
'2.5': __build_magic(62131),
'2.6': __build_magic(62161),
'2.7': __build_magic(62191),
'3.0': __build_magic(3000),
'3.1': __build_magic(3141),
'3.2': __build_magic(3160),
'3.3': __build_magic(3190),
'3.4': __build_magic(3250),
'3.5': __build_magic(3350),
'3.6': __build_magic(3360),
'3.7': __build_magic(3390),
}
def __timestamp():
"""Generate timestamp data for pyc header."""
today = time.time()
ret = struct.pack(b'=L', int(today))
return ret
def __source_size(size):
"""Generate source code size data for pyc header."""
ret = struct.pack(b'=L', int(size))
return ret
def __current_magic():
"""Current Python magic number."""
return imp.get_magic()
def _get_scripts_resource(pe):
"""Return the PYTHONSCRIPT resource entry."""
res = None
for entry in pe.DIRECTORY_ENTRY_RESOURCE.entries:
if entry.name and entry.name.string == b"PYTHONSCRIPT":
res = entry.directory.entries[0].directory.entries[0]
break
return res
def _resource_dump(pe, res):
"""Return the dump of the given resource."""
rva = res.data.struct.OffsetToData
size = res.data.struct.Size
dump = pe.get_data(rva, size)
return dump
def _get_co_from_dump(data):
"""Return the code objects from the dump."""
# Read py2exe header
current = struct.calcsize(b'iiii')
metadata = struct.unpack(b'iiii', data[:current])
# check py2exe magic number
# assert(metadata[0] == 0x78563412)
logging.info("Magic value: %x", metadata[0])
logging.info("Code bytes length: %d", metadata[3])
arcname = ''
while six.indexbytes(data, current) != 0:
arcname += chr(six.indexbytes(data, current))
current += 1
logging.info("Archive name: %s", arcname or '-')
code_bytes = data[current + 1:]
# verify code bytes count and metadata info
# assert(len(code_bytes) == metadata[3])
code_objects = marshal.loads(code_bytes)
return code_objects
def check_py2exe_file(pe):
"""Check file is a py2exe executable."""
py2exe_resource = _get_scripts_resource(pe)
if py2exe_resource is None:
logging.info('This is not a py2exe executable.')
if pe.__data__.find(b'pyi-windows-manifest-filename'):
logging.info('This seems a pyinstaller executable (unsupported).')
return bool(py2exe_resource)
def extract_code_objects(pe):
"""Extract Python code objects from a py2exe executable."""
script_res = _get_scripts_resource(pe)
dump = _resource_dump(pe, script_res)
return _get_co_from_dump(dump)
def _generate_pyc_header(python_version, size):
if python_version is None:
version = __current_magic()
version_tuple = sys.version_info
else:
version = PYTHON_MAGIC_WORDS.get(python_version[:3], __current_magic())
version_tuple = tuple(map(int, python_version.split('.')))
header = version + __timestamp()
if version_tuple[0] == 3 and version_tuple[1] >= 3:
# source code size was added to pyc header since Python 3.3
header += __source_size(size)
return header
def dump_to_pyc(co, python_version, output_dir):
"""Save given code_object as a .pyc file."""
# assume Windows path information from the .exe
pyc_basename = ntpath.basename(co.co_filename)
pyc_name = pyc_basename + '.pyc'
if pyc_name not in IGNORE:
logging.info("Extracting %s", pyc_name)
pyc_header = _generate_pyc_header(python_version, len(co.co_code))
destination = os.path.join(output_dir, pyc_name)
pyc = open(destination, 'wb')
pyc.write(pyc_header)
marshaled_code = marshal.dumps(co)
pyc.write(marshaled_code)
pyc.close()
else:
logging.info("Skipping %s", pyc_name)
def unpy2exe(filename, python_version=None, output_dir=None):
"""Process input params and produce output pyc files."""
if output_dir is None:
output_dir = '.'
elif not os.path.exists(output_dir):
os.makedirs(output_dir)
pe = pefile.PE(filename)
is_py2exe = check_py2exe_file(pe)
if not is_py2exe:
raise ValueError('Not a py2exe executable.')
code_objects = extract_code_objects(pe)
for co in code_objects:
dump_to_pyc(co, python_version, output_dir)