1#-----------------------------------------------------------------------------
2# Copyright (c) 2013-2019, PyInstaller Development Team.
3#
4# Distributed under the terms of the GNU General Public License with exception
5# for distributing bootloader.
6#
7# The full license is in the file COPYING.txt, distributed with this software.
8#-----------------------------------------------------------------------------
9
10
11"""
12Viewer for archives packaged by archive.py
13"""
14
15from __future__ import print_function
16
17import argparse
18import os
19import pprint
20import tempfile
21import zlib
22
23from PyInstaller.loader import pyimod02_archive
24from PyInstaller.archive.readers import CArchiveReader, NotAnArchiveError
25from PyInstaller.compat import stdin_input
26import PyInstaller.log
27
28stack = []
29cleanup = []
30
31
32def main(name, brief, debug, rec_debug, **unused_options):
33
34    global stack
35
36    if not os.path.isfile(name):
37        print(name, "is an invalid file name!")
38        return 1
39
40    arch = get_archive(name)
41    stack.append((name, arch))
42    if debug or brief:
43        show_log(arch, rec_debug, brief)
44        raise SystemExit(0)
45    else:
46        show(name, arch)
47
48    while 1:
49        try:
50            toks = stdin_input('? ').split(None, 1)
51        except EOFError:
52            # Ctrl-D
53            print()  # Clear line.
54            break
55        if not toks:
56            usage()
57            continue
58        if len(toks) == 1:
59            cmd = toks[0]
60            arg = ''
61        else:
62            cmd, arg = toks
63        cmd = cmd.upper()
64        if cmd == 'U':
65            if len(stack) > 1:
66                arch = stack[-1][1]
67                arch.lib.close()
68                del stack[-1]
69            name, arch = stack[-1]
70            show(name, arch)
71        elif cmd == 'O':
72            if not arg:
73                arg = stdin_input('open name? ')
74            arg = arg.strip()
75            try:
76                arch = get_archive(arg)
77            except NotAnArchiveError as e:
78                print(e)
79                continue
80            if arch is None:
81                print(arg, "not found")
82                continue
83            stack.append((arg, arch))
84            show(arg, arch)
85        elif cmd == 'X':
86            if not arg:
87                arg = stdin_input('extract name? ')
88            arg = arg.strip()
89            data = get_data(arg, arch)
90            if data is None:
91                print("Not found")
92                continue
93            filename = stdin_input('to filename? ')
94            if not filename:
95                print(repr(data))
96            else:
97                with open(filename, 'wb') as fp:
98                    fp.write(data)
99        elif cmd == 'Q':
100            break
101        else:
102            usage()
103    do_cleanup()
104
105
106def do_cleanup():
107    global stack, cleanup
108    for (name, arch) in stack:
109        arch.lib.close()
110    stack = []
111    for filename in cleanup:
112        try:
113            os.remove(filename)
114        except Exception as e:
115            print("couldn't delete", filename, e.args)
116    cleanup = []
117
118
119def usage():
120    print("U: go Up one level")
121    print("O <name>: open embedded archive name")
122    print("X <name>: extract name")
123    print("Q: quit")
124
125
126def get_archive(name):
127    if not stack:
128        if name[-4:].lower() == '.pyz':
129            return ZlibArchive(name)
130        return CArchiveReader(name)
131    parent = stack[-1][1]
132    try:
133        return parent.openEmbedded(name)
134    except KeyError:
135        return None
136    except (ValueError, RuntimeError):
137        ndx = parent.toc.find(name)
138        dpos, dlen, ulen, flag, typcd, name = parent.toc[ndx]
139        x, data = parent.extract(ndx)
140        tempfilename = tempfile.mktemp()
141        cleanup.append(tempfilename)
142        with open(tempfilename, 'wb') as fp:
143            fp.write(data)
144        if typcd == 'z':
145            return ZlibArchive(tempfilename)
146        else:
147            return CArchiveReader(tempfilename)
148
149
150def get_data(name, arch):
151    if isinstance(arch.toc, dict):
152        (ispkg, pos, length) = arch.toc.get(name, (0, None, 0))
153        if pos is None:
154            return None
155        with arch.lib:
156            arch.lib.seek(arch.start + pos)
157            return zlib.decompress(arch.lib.read(length))
158    ndx = arch.toc.find(name)
159    dpos, dlen, ulen, flag, typcd, name = arch.toc[ndx]
160    x, data = arch.extract(ndx)
161    return data
162
163
164def show(name, arch):
165    if isinstance(arch.toc, dict):
166        print(" Name: (ispkg, pos, len)")
167        toc = arch.toc
168    else:
169        print(" pos, length, uncompressed, iscompressed, type, name")
170        toc = arch.toc.data
171    pprint.pprint(toc)
172
173
174def get_content(arch, recursive, brief, output):
175    if isinstance(arch.toc, dict):
176        toc = arch.toc
177        if brief:
178            for name, _ in toc.items():
179                output.append(name)
180        else:
181            output.append(toc)
182    else:
183        toc = arch.toc.data
184        for el in toc:
185            if brief:
186                output.append(el[5])
187            else:
188                output.append(el)
189            if recursive:
190                if el[4] in ('z', 'a'):
191                    get_content(get_archive(el[5]), recursive, brief, output)
192                    stack.pop()
193
194
195def show_log(arch, recursive, brief):
196    output = []
197    get_content(arch, recursive, brief, output)
198    # first print all TOCs
199    for out in output:
200        if isinstance(out, dict):
201            pprint.pprint(out)
202    # then print the other entries
203    pprint.pprint([out for out in output if not isinstance(out, dict)])
204
205
206def get_archive_content(filename):
207    """
208    Get a list of the (recursive) content of archive `filename`.
209
210    This function is primary meant to be used by runtests.
211    """
212    archive = get_archive(filename)
213    stack.append((filename, archive))
214    output = []
215    get_content(archive, recursive=True, brief=True, output=output)
216    do_cleanup()
217    return output
218
219
220class ZlibArchive(pyimod02_archive.ZlibArchiveReader):
221
222    def checkmagic(self):
223        """ Overridable.
224            Check to see if the file object self.lib actually has a file
225            we understand.
226        """
227        self.lib.seek(self.start)  # default - magic is at start of file.
228        if self.lib.read(len(self.MAGIC)) != self.MAGIC:
229            raise RuntimeError("%s is not a valid %s archive file"
230                               % (self.path, self.__class__.__name__))
231        if self.lib.read(len(self.pymagic)) != self.pymagic:
232            print("Warning: pyz is from a different Python version")
233        self.lib.read(4)
234
235
236def run():
237    parser = argparse.ArgumentParser()
238    parser.add_argument('-l', '--log',
239                        default=False,
240                        action='store_true',
241                        dest='debug',
242                        help='Print an archive log (default: %(default)s)')
243    parser.add_argument('-r', '--recursive',
244                        default=False,
245                        action='store_true',
246                        dest='rec_debug',
247                        help='Recursively print an archive log (default: %(default)s). '
248                        'Can be combined with -r')
249    parser.add_argument('-b', '--brief',
250                        default=False,
251                        action='store_true',
252                        dest='brief',
253                        help='Print only file name. (default: %(default)s). '
254                        'Can be combined with -r')
255    PyInstaller.log.__add_options(parser)
256    parser.add_argument('name', metavar='pyi_archive',
257                        help="pyinstaller archive to show content of")
258
259    args = parser.parse_args()
260    PyInstaller.log.__process_options(parser, args)
261
262    try:
263        raise SystemExit(main(**vars(args)))
264    except KeyboardInterrupt:
265        raise SystemExit("Aborted by user request.")
266
267if __name__ == '__main__':
268    run()
269
270