1"""zipimport provides support for importing Python modules from Zip archives.
2
3This module exports three objects:
4- zipimporter: a class; its constructor takes a path to a Zip archive.
5- ZipImportError: exception raised by zipimporter objects. It's a
6  subclass of ImportError, so it can be caught as ImportError, too.
7- _zip_directory_cache: a dict, mapping archive paths to zip directory
8  info dicts, as used in zipimporter._files.
9
10It is usually not needed to use the zipimport module explicitly; it is
11used by the builtin import mechanism for sys.path items that are paths
12to Zip archives.
13"""
14
15#from importlib import _bootstrap_external
16#from importlib import _bootstrap  # for _verbose_message
17import _frozen_importlib_external as _bootstrap_external
18from _frozen_importlib_external import _unpack_uint16, _unpack_uint32
19import _frozen_importlib as _bootstrap  # for _verbose_message
20import _imp  # for check_hash_based_pycs
21import _io  # for open
22import marshal  # for loads
23import sys  # for modules
24import time  # for mktime
25
26__all__ = ['ZipImportError', 'zipimporter']
27
28
29path_sep = _bootstrap_external.path_sep
30alt_path_sep = _bootstrap_external.path_separators[1:]
31
32
33class ZipImportError(ImportError):
34    pass
35
36# _read_directory() cache
37_zip_directory_cache = {}
38
39_module_type = type(sys)
40
41END_CENTRAL_DIR_SIZE = 22
42STRING_END_ARCHIVE = b'PK\x05\x06'
43MAX_COMMENT_LEN = (1 << 16) - 1
44
45class zipimporter:
46    """zipimporter(archivepath) -> zipimporter object
47
48    Create a new zipimporter instance. 'archivepath' must be a path to
49    a zipfile, or to a specific path inside a zipfile. For example, it can be
50    '/tmp/myimport.zip', or '/tmp/myimport.zip/mydirectory', if mydirectory is a
51    valid directory inside the archive.
52
53    'ZipImportError is raised if 'archivepath' doesn't point to a valid Zip
54    archive.
55
56    The 'archive' attribute of zipimporter objects contains the name of the
57    zipfile targeted.
58    """
59
60    # Split the "subdirectory" from the Zip archive path, lookup a matching
61    # entry in sys.path_importer_cache, fetch the file directory from there
62    # if found, or else read it from the archive.
63    def __init__(self, path):
64        if not isinstance(path, str):
65            import os
66            path = os.fsdecode(path)
67        if not path:
68            raise ZipImportError('archive path is empty', path=path)
69        if alt_path_sep:
70            path = path.replace(alt_path_sep, path_sep)
71
72        prefix = []
73        while True:
74            try:
75                st = _bootstrap_external._path_stat(path)
76            except (OSError, ValueError):
77                # On Windows a ValueError is raised for too long paths.
78                # Back up one path element.
79                dirname, basename = _bootstrap_external._path_split(path)
80                if dirname == path:
81                    raise ZipImportError('not a Zip file', path=path)
82                path = dirname
83                prefix.append(basename)
84            else:
85                # it exists
86                if (st.st_mode & 0o170000) != 0o100000:  # stat.S_ISREG
87                    # it's a not file
88                    raise ZipImportError('not a Zip file', path=path)
89                break
90
91        try:
92            files = _zip_directory_cache[path]
93        except KeyError:
94            files = _read_directory(path)
95            _zip_directory_cache[path] = files
96        self._files = files
97        self.archive = path
98        # a prefix directory following the ZIP file path.
99        self.prefix = _bootstrap_external._path_join(*prefix[::-1])
100        if self.prefix:
101            self.prefix += path_sep
102
103
104    # Check whether we can satisfy the import of the module named by
105    # 'fullname', or whether it could be a portion of a namespace
106    # package. Return self if we can load it, a string containing the
107    # full path if it's a possible namespace portion, None if we
108    # can't load it.
109    def find_loader(self, fullname, path=None):
110        """find_loader(fullname, path=None) -> self, str or None.
111
112        Search for a module specified by 'fullname'. 'fullname' must be the
113        fully qualified (dotted) module name. It returns the zipimporter
114        instance itself if the module was found, a string containing the
115        full path name if it's possibly a portion of a namespace package,
116        or None otherwise. The optional 'path' argument is ignored -- it's
117        there for compatibility with the importer protocol.
118        """
119        mi = _get_module_info(self, fullname)
120        if mi is not None:
121            # This is a module or package.
122            return self, []
123
124        # Not a module or regular package. See if this is a directory, and
125        # therefore possibly a portion of a namespace package.
126
127        # We're only interested in the last path component of fullname
128        # earlier components are recorded in self.prefix.
129        modpath = _get_module_path(self, fullname)
130        if _is_dir(self, modpath):
131            # This is possibly a portion of a namespace
132            # package. Return the string representing its path,
133            # without a trailing separator.
134            return None, [f'{self.archive}{path_sep}{modpath}']
135
136        return None, []
137
138
139    # Check whether we can satisfy the import of the module named by
140    # 'fullname'. Return self if we can, None if we can't.
141    def find_module(self, fullname, path=None):
142        """find_module(fullname, path=None) -> self or None.
143
144        Search for a module specified by 'fullname'. 'fullname' must be the
145        fully qualified (dotted) module name. It returns the zipimporter
146        instance itself if the module was found, or None if it wasn't.
147        The optional 'path' argument is ignored -- it's there for compatibility
148        with the importer protocol.
149        """
150        return self.find_loader(fullname, path)[0]
151
152
153    def get_code(self, fullname):
154        """get_code(fullname) -> code object.
155
156        Return the code object for the specified module. Raise ZipImportError
157        if the module couldn't be found.
158        """
159        code, ispackage, modpath = _get_module_code(self, fullname)
160        return code
161
162
163    def get_data(self, pathname):
164        """get_data(pathname) -> string with file data.
165
166        Return the data associated with 'pathname'. Raise OSError if
167        the file wasn't found.
168        """
169        if alt_path_sep:
170            pathname = pathname.replace(alt_path_sep, path_sep)
171
172        key = pathname
173        if pathname.startswith(self.archive + path_sep):
174            key = pathname[len(self.archive + path_sep):]
175
176        try:
177            toc_entry = self._files[key]
178        except KeyError:
179            raise OSError(0, '', key)
180        return _get_data(self.archive, toc_entry)
181
182
183    # Return a string matching __file__ for the named module
184    def get_filename(self, fullname):
185        """get_filename(fullname) -> filename string.
186
187        Return the filename for the specified module.
188        """
189        # Deciding the filename requires working out where the code
190        # would come from if the module was actually loaded
191        code, ispackage, modpath = _get_module_code(self, fullname)
192        return modpath
193
194
195    def get_source(self, fullname):
196        """get_source(fullname) -> source string.
197
198        Return the source code for the specified module. Raise ZipImportError
199        if the module couldn't be found, return None if the archive does
200        contain the module, but has no source for it.
201        """
202        mi = _get_module_info(self, fullname)
203        if mi is None:
204            raise ZipImportError(f"can't find module {fullname!r}", name=fullname)
205
206        path = _get_module_path(self, fullname)
207        if mi:
208            fullpath = _bootstrap_external._path_join(path, '__init__.py')
209        else:
210            fullpath = f'{path}.py'
211
212        try:
213            toc_entry = self._files[fullpath]
214        except KeyError:
215            # we have the module, but no source
216            return None
217        return _get_data(self.archive, toc_entry).decode()
218
219
220    # Return a bool signifying whether the module is a package or not.
221    def is_package(self, fullname):
222        """is_package(fullname) -> bool.
223
224        Return True if the module specified by fullname is a package.
225        Raise ZipImportError if the module couldn't be found.
226        """
227        mi = _get_module_info(self, fullname)
228        if mi is None:
229            raise ZipImportError(f"can't find module {fullname!r}", name=fullname)
230        return mi
231
232
233    # Load and return the module named by 'fullname'.
234    def load_module(self, fullname):
235        """load_module(fullname) -> module.
236
237        Load the module specified by 'fullname'. 'fullname' must be the
238        fully qualified (dotted) module name. It returns the imported
239        module, or raises ZipImportError if it wasn't found.
240        """
241        code, ispackage, modpath = _get_module_code(self, fullname)
242        mod = sys.modules.get(fullname)
243        if mod is None or not isinstance(mod, _module_type):
244            mod = _module_type(fullname)
245            sys.modules[fullname] = mod
246        mod.__loader__ = self
247
248        try:
249            if ispackage:
250                # add __path__ to the module *before* the code gets
251                # executed
252                path = _get_module_path(self, fullname)
253                fullpath = _bootstrap_external._path_join(self.archive, path)
254                mod.__path__ = [fullpath]
255
256            if not hasattr(mod, '__builtins__'):
257                mod.__builtins__ = __builtins__
258            _bootstrap_external._fix_up_module(mod.__dict__, fullname, modpath)
259            exec(code, mod.__dict__)
260        except:
261            del sys.modules[fullname]
262            raise
263
264        try:
265            mod = sys.modules[fullname]
266        except KeyError:
267            raise ImportError(f'Loaded module {fullname!r} not found in sys.modules')
268        _bootstrap._verbose_message('import {} # loaded from Zip {}', fullname, modpath)
269        return mod
270
271
272    def get_resource_reader(self, fullname):
273        """Return the ResourceReader for a package in a zip file.
274
275        If 'fullname' is a package within the zip file, return the
276        'ResourceReader' object for the package.  Otherwise return None.
277        """
278        try:
279            if not self.is_package(fullname):
280                return None
281        except ZipImportError:
282            return None
283        if not _ZipImportResourceReader._registered:
284            from importlib.abc import ResourceReader
285            ResourceReader.register(_ZipImportResourceReader)
286            _ZipImportResourceReader._registered = True
287        return _ZipImportResourceReader(self, fullname)
288
289
290    def __repr__(self):
291        return f'<zipimporter object "{self.archive}{path_sep}{self.prefix}">'
292
293
294# _zip_searchorder defines how we search for a module in the Zip
295# archive: we first search for a package __init__, then for
296# non-package .pyc, and .py entries. The .pyc entries
297# are swapped by initzipimport() if we run in optimized mode. Also,
298# '/' is replaced by path_sep there.
299_zip_searchorder = (
300    (path_sep + '__init__.pyc', True, True),
301    (path_sep + '__init__.py', False, True),
302    ('.pyc', True, False),
303    ('.py', False, False),
304)
305
306# Given a module name, return the potential file path in the
307# archive (without extension).
308def _get_module_path(self, fullname):
309    return self.prefix + fullname.rpartition('.')[2]
310
311# Does this path represent a directory?
312def _is_dir(self, path):
313    # See if this is a "directory". If so, it's eligible to be part
314    # of a namespace package. We test by seeing if the name, with an
315    # appended path separator, exists.
316    dirpath = path + path_sep
317    # If dirpath is present in self._files, we have a directory.
318    return dirpath in self._files
319
320# Return some information about a module.
321def _get_module_info(self, fullname):
322    path = _get_module_path(self, fullname)
323    for suffix, isbytecode, ispackage in _zip_searchorder:
324        fullpath = path + suffix
325        if fullpath in self._files:
326            return ispackage
327    return None
328
329
330# implementation
331
332# _read_directory(archive) -> files dict (new reference)
333#
334# Given a path to a Zip archive, build a dict, mapping file names
335# (local to the archive, using SEP as a separator) to toc entries.
336#
337# A toc_entry is a tuple:
338#
339# (__file__,        # value to use for __file__, available for all files,
340#                   # encoded to the filesystem encoding
341#  compress,        # compression kind; 0 for uncompressed
342#  data_size,       # size of compressed data on disk
343#  file_size,       # size of decompressed data
344#  file_offset,     # offset of file header from start of archive
345#  time,            # mod time of file (in dos format)
346#  date,            # mod data of file (in dos format)
347#  crc,             # crc checksum of the data
348# )
349#
350# Directories can be recognized by the trailing path_sep in the name,
351# data_size and file_offset are 0.
352def _read_directory(archive):
353    try:
354        fp = _io.open_code(archive)
355    except OSError:
356        raise ZipImportError(f"can't open Zip file: {archive!r}", path=archive)
357
358    with fp:
359        try:
360            fp.seek(-END_CENTRAL_DIR_SIZE, 2)
361            header_position = fp.tell()
362            buffer = fp.read(END_CENTRAL_DIR_SIZE)
363        except OSError:
364            raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
365        if len(buffer) != END_CENTRAL_DIR_SIZE:
366            raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
367        if buffer[:4] != STRING_END_ARCHIVE:
368            # Bad: End of Central Dir signature
369            # Check if there's a comment.
370            try:
371                fp.seek(0, 2)
372                file_size = fp.tell()
373            except OSError:
374                raise ZipImportError(f"can't read Zip file: {archive!r}",
375                                     path=archive)
376            max_comment_start = max(file_size - MAX_COMMENT_LEN -
377                                    END_CENTRAL_DIR_SIZE, 0)
378            try:
379                fp.seek(max_comment_start)
380                data = fp.read()
381            except OSError:
382                raise ZipImportError(f"can't read Zip file: {archive!r}",
383                                     path=archive)
384            pos = data.rfind(STRING_END_ARCHIVE)
385            if pos < 0:
386                raise ZipImportError(f'not a Zip file: {archive!r}',
387                                     path=archive)
388            buffer = data[pos:pos+END_CENTRAL_DIR_SIZE]
389            if len(buffer) != END_CENTRAL_DIR_SIZE:
390                raise ZipImportError(f"corrupt Zip file: {archive!r}",
391                                     path=archive)
392            header_position = file_size - len(data) + pos
393
394        header_size = _unpack_uint32(buffer[12:16])
395        header_offset = _unpack_uint32(buffer[16:20])
396        if header_position < header_size:
397            raise ZipImportError(f'bad central directory size: {archive!r}', path=archive)
398        if header_position < header_offset:
399            raise ZipImportError(f'bad central directory offset: {archive!r}', path=archive)
400        header_position -= header_size
401        arc_offset = header_position - header_offset
402        if arc_offset < 0:
403            raise ZipImportError(f'bad central directory size or offset: {archive!r}', path=archive)
404
405        files = {}
406        # Start of Central Directory
407        count = 0
408        try:
409            fp.seek(header_position)
410        except OSError:
411            raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
412        while True:
413            buffer = fp.read(46)
414            if len(buffer) < 4:
415                raise EOFError('EOF read where not expected')
416            # Start of file header
417            if buffer[:4] != b'PK\x01\x02':
418                break                                # Bad: Central Dir File Header
419            if len(buffer) != 46:
420                raise EOFError('EOF read where not expected')
421            flags = _unpack_uint16(buffer[8:10])
422            compress = _unpack_uint16(buffer[10:12])
423            time = _unpack_uint16(buffer[12:14])
424            date = _unpack_uint16(buffer[14:16])
425            crc = _unpack_uint32(buffer[16:20])
426            data_size = _unpack_uint32(buffer[20:24])
427            file_size = _unpack_uint32(buffer[24:28])
428            name_size = _unpack_uint16(buffer[28:30])
429            extra_size = _unpack_uint16(buffer[30:32])
430            comment_size = _unpack_uint16(buffer[32:34])
431            file_offset = _unpack_uint32(buffer[42:46])
432            header_size = name_size + extra_size + comment_size
433            if file_offset > header_offset:
434                raise ZipImportError(f'bad local header offset: {archive!r}', path=archive)
435            file_offset += arc_offset
436
437            try:
438                name = fp.read(name_size)
439            except OSError:
440                raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
441            if len(name) != name_size:
442                raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
443            # On Windows, calling fseek to skip over the fields we don't use is
444            # slower than reading the data because fseek flushes stdio's
445            # internal buffers.    See issue #8745.
446            try:
447                if len(fp.read(header_size - name_size)) != header_size - name_size:
448                    raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
449            except OSError:
450                raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
451
452            if flags & 0x800:
453                # UTF-8 file names extension
454                name = name.decode()
455            else:
456                # Historical ZIP filename encoding
457                try:
458                    name = name.decode('ascii')
459                except UnicodeDecodeError:
460                    name = name.decode('latin1').translate(cp437_table)
461
462            name = name.replace('/', path_sep)
463            path = _bootstrap_external._path_join(archive, name)
464            t = (path, compress, data_size, file_size, file_offset, time, date, crc)
465            files[name] = t
466            count += 1
467    _bootstrap._verbose_message('zipimport: found {} names in {!r}', count, archive)
468    return files
469
470# During bootstrap, we may need to load the encodings
471# package from a ZIP file. But the cp437 encoding is implemented
472# in Python in the encodings package.
473#
474# Break out of this dependency by using the translation table for
475# the cp437 encoding.
476cp437_table = (
477    # ASCII part, 8 rows x 16 chars
478    '\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f'
479    '\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f'
480    ' !"#$%&\'()*+,-./'
481    '0123456789:;<=>?'
482    '@ABCDEFGHIJKLMNO'
483    'PQRSTUVWXYZ[\\]^_'
484    '`abcdefghijklmno'
485    'pqrstuvwxyz{|}~\x7f'
486    # non-ASCII part, 16 rows x 8 chars
487    '\xc7\xfc\xe9\xe2\xe4\xe0\xe5\xe7'
488    '\xea\xeb\xe8\xef\xee\xec\xc4\xc5'
489    '\xc9\xe6\xc6\xf4\xf6\xf2\xfb\xf9'
490    '\xff\xd6\xdc\xa2\xa3\xa5\u20a7\u0192'
491    '\xe1\xed\xf3\xfa\xf1\xd1\xaa\xba'
492    '\xbf\u2310\xac\xbd\xbc\xa1\xab\xbb'
493    '\u2591\u2592\u2593\u2502\u2524\u2561\u2562\u2556'
494    '\u2555\u2563\u2551\u2557\u255d\u255c\u255b\u2510'
495    '\u2514\u2534\u252c\u251c\u2500\u253c\u255e\u255f'
496    '\u255a\u2554\u2569\u2566\u2560\u2550\u256c\u2567'
497    '\u2568\u2564\u2565\u2559\u2558\u2552\u2553\u256b'
498    '\u256a\u2518\u250c\u2588\u2584\u258c\u2590\u2580'
499    '\u03b1\xdf\u0393\u03c0\u03a3\u03c3\xb5\u03c4'
500    '\u03a6\u0398\u03a9\u03b4\u221e\u03c6\u03b5\u2229'
501    '\u2261\xb1\u2265\u2264\u2320\u2321\xf7\u2248'
502    '\xb0\u2219\xb7\u221a\u207f\xb2\u25a0\xa0'
503)
504
505_importing_zlib = False
506
507# Return the zlib.decompress function object, or NULL if zlib couldn't
508# be imported. The function is cached when found, so subsequent calls
509# don't import zlib again.
510def _get_decompress_func():
511    global _importing_zlib
512    if _importing_zlib:
513        # Someone has a zlib.py[co] in their Zip file
514        # let's avoid a stack overflow.
515        _bootstrap._verbose_message('zipimport: zlib UNAVAILABLE')
516        raise ZipImportError("can't decompress data; zlib not available")
517
518    _importing_zlib = True
519    try:
520        from zlib import decompress
521    except Exception:
522        _bootstrap._verbose_message('zipimport: zlib UNAVAILABLE')
523        raise ZipImportError("can't decompress data; zlib not available")
524    finally:
525        _importing_zlib = False
526
527    _bootstrap._verbose_message('zipimport: zlib available')
528    return decompress
529
530# Given a path to a Zip file and a toc_entry, return the (uncompressed) data.
531def _get_data(archive, toc_entry):
532    datapath, compress, data_size, file_size, file_offset, time, date, crc = toc_entry
533    if data_size < 0:
534        raise ZipImportError('negative data size')
535
536    with _io.open_code(archive) as fp:
537        # Check to make sure the local file header is correct
538        try:
539            fp.seek(file_offset)
540        except OSError:
541            raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
542        buffer = fp.read(30)
543        if len(buffer) != 30:
544            raise EOFError('EOF read where not expected')
545
546        if buffer[:4] != b'PK\x03\x04':
547            # Bad: Local File Header
548            raise ZipImportError(f'bad local file header: {archive!r}', path=archive)
549
550        name_size = _unpack_uint16(buffer[26:28])
551        extra_size = _unpack_uint16(buffer[28:30])
552        header_size = 30 + name_size + extra_size
553        file_offset += header_size  # Start of file data
554        try:
555            fp.seek(file_offset)
556        except OSError:
557            raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
558        raw_data = fp.read(data_size)
559        if len(raw_data) != data_size:
560            raise OSError("zipimport: can't read data")
561
562    if compress == 0:
563        # data is not compressed
564        return raw_data
565
566    # Decompress with zlib
567    try:
568        decompress = _get_decompress_func()
569    except Exception:
570        raise ZipImportError("can't decompress data; zlib not available")
571    return decompress(raw_data, -15)
572
573
574# Lenient date/time comparison function. The precision of the mtime
575# in the archive is lower than the mtime stored in a .pyc: we
576# must allow a difference of at most one second.
577def _eq_mtime(t1, t2):
578    # dostime only stores even seconds, so be lenient
579    return abs(t1 - t2) <= 1
580
581
582# Given the contents of a .py[co] file, unmarshal the data
583# and return the code object. Return None if it the magic word doesn't
584# match, or if the recorded .py[co] metadata does not match the source,
585# (we do this instead of raising an exception as we fall back
586# to .py if available and we don't want to mask other errors).
587def _unmarshal_code(self, pathname, fullpath, fullname, data):
588    exc_details = {
589        'name': fullname,
590        'path': fullpath,
591    }
592
593    try:
594        flags = _bootstrap_external._classify_pyc(data, fullname, exc_details)
595    except ImportError:
596        return None
597
598    hash_based = flags & 0b1 != 0
599    if hash_based:
600        check_source = flags & 0b10 != 0
601        if (_imp.check_hash_based_pycs != 'never' and
602                (check_source or _imp.check_hash_based_pycs == 'always')):
603            source_bytes = _get_pyc_source(self, fullpath)
604            if source_bytes is not None:
605                source_hash = _imp.source_hash(
606                    _bootstrap_external._RAW_MAGIC_NUMBER,
607                    source_bytes,
608                )
609
610                try:
611                    _bootstrap_external._validate_hash_pyc(
612                        data, source_hash, fullname, exc_details)
613                except ImportError:
614                    return None
615    else:
616        source_mtime, source_size = \
617            _get_mtime_and_size_of_source(self, fullpath)
618
619        if source_mtime:
620            # We don't use _bootstrap_external._validate_timestamp_pyc
621            # to allow for a more lenient timestamp check.
622            if (not _eq_mtime(_unpack_uint32(data[8:12]), source_mtime) or
623                    _unpack_uint32(data[12:16]) != source_size):
624                _bootstrap._verbose_message(
625                    f'bytecode is stale for {fullname!r}')
626                return None
627
628    code = marshal.loads(data[16:])
629    if not isinstance(code, _code_type):
630        raise TypeError(f'compiled module {pathname!r} is not a code object')
631    return code
632
633_code_type = type(_unmarshal_code.__code__)
634
635
636# Replace any occurrences of '\r\n?' in the input string with '\n'.
637# This converts DOS and Mac line endings to Unix line endings.
638def _normalize_line_endings(source):
639    source = source.replace(b'\r\n', b'\n')
640    source = source.replace(b'\r', b'\n')
641    return source
642
643# Given a string buffer containing Python source code, compile it
644# and return a code object.
645def _compile_source(pathname, source):
646    source = _normalize_line_endings(source)
647    return compile(source, pathname, 'exec', dont_inherit=True)
648
649# Convert the date/time values found in the Zip archive to a value
650# that's compatible with the time stamp stored in .pyc files.
651def _parse_dostime(d, t):
652    return time.mktime((
653        (d >> 9) + 1980,    # bits 9..15: year
654        (d >> 5) & 0xF,     # bits 5..8: month
655        d & 0x1F,           # bits 0..4: day
656        t >> 11,            # bits 11..15: hours
657        (t >> 5) & 0x3F,    # bits 8..10: minutes
658        (t & 0x1F) * 2,     # bits 0..7: seconds / 2
659        -1, -1, -1))
660
661# Given a path to a .pyc file in the archive, return the
662# modification time of the matching .py file and its size,
663# or (0, 0) if no source is available.
664def _get_mtime_and_size_of_source(self, path):
665    try:
666        # strip 'c' or 'o' from *.py[co]
667        assert path[-1:] in ('c', 'o')
668        path = path[:-1]
669        toc_entry = self._files[path]
670        # fetch the time stamp of the .py file for comparison
671        # with an embedded pyc time stamp
672        time = toc_entry[5]
673        date = toc_entry[6]
674        uncompressed_size = toc_entry[3]
675        return _parse_dostime(date, time), uncompressed_size
676    except (KeyError, IndexError, TypeError):
677        return 0, 0
678
679
680# Given a path to a .pyc file in the archive, return the
681# contents of the matching .py file, or None if no source
682# is available.
683def _get_pyc_source(self, path):
684    # strip 'c' or 'o' from *.py[co]
685    assert path[-1:] in ('c', 'o')
686    path = path[:-1]
687
688    try:
689        toc_entry = self._files[path]
690    except KeyError:
691        return None
692    else:
693        return _get_data(self.archive, toc_entry)
694
695
696# Get the code object associated with the module specified by
697# 'fullname'.
698def _get_module_code(self, fullname):
699    path = _get_module_path(self, fullname)
700    for suffix, isbytecode, ispackage in _zip_searchorder:
701        fullpath = path + suffix
702        _bootstrap._verbose_message('trying {}{}{}', self.archive, path_sep, fullpath, verbosity=2)
703        try:
704            toc_entry = self._files[fullpath]
705        except KeyError:
706            pass
707        else:
708            modpath = toc_entry[0]
709            data = _get_data(self.archive, toc_entry)
710            if isbytecode:
711                code = _unmarshal_code(self, modpath, fullpath, fullname, data)
712            else:
713                code = _compile_source(modpath, data)
714            if code is None:
715                # bad magic number or non-matching mtime
716                # in byte code, try next
717                continue
718            modpath = toc_entry[0]
719            return code, ispackage, modpath
720    else:
721        raise ZipImportError(f"can't find module {fullname!r}", name=fullname)
722
723
724class _ZipImportResourceReader:
725    """Private class used to support ZipImport.get_resource_reader().
726
727    This class is allowed to reference all the innards and private parts of
728    the zipimporter.
729    """
730    _registered = False
731
732    def __init__(self, zipimporter, fullname):
733        self.zipimporter = zipimporter
734        self.fullname = fullname
735
736    def open_resource(self, resource):
737        fullname_as_path = self.fullname.replace('.', '/')
738        path = f'{fullname_as_path}/{resource}'
739        from io import BytesIO
740        try:
741            return BytesIO(self.zipimporter.get_data(path))
742        except OSError:
743            raise FileNotFoundError(path)
744
745    def resource_path(self, resource):
746        # All resources are in the zip file, so there is no path to the file.
747        # Raising FileNotFoundError tells the higher level API to extract the
748        # binary data and create a temporary file.
749        raise FileNotFoundError
750
751    def is_resource(self, name):
752        # Maybe we could do better, but if we can get the data, it's a
753        # resource.  Otherwise it isn't.
754        fullname_as_path = self.fullname.replace('.', '/')
755        path = f'{fullname_as_path}/{name}'
756        try:
757            self.zipimporter.get_data(path)
758        except OSError:
759            return False
760        return True
761
762    def contents(self):
763        # This is a bit convoluted, because fullname will be a module path,
764        # but _files is a list of file names relative to the top of the
765        # archive's namespace.  We want to compare file paths to find all the
766        # names of things inside the module represented by fullname.  So we
767        # turn the module path of fullname into a file path relative to the
768        # top of the archive, and then we iterate through _files looking for
769        # names inside that "directory".
770        from pathlib import Path
771        fullname_path = Path(self.zipimporter.get_filename(self.fullname))
772        relative_path = fullname_path.relative_to(self.zipimporter.archive)
773        # Don't forget that fullname names a package, so its path will include
774        # __init__.py, which we want to ignore.
775        assert relative_path.name == '__init__.py'
776        package_path = relative_path.parent
777        subdirs_seen = set()
778        for filename in self.zipimporter._files:
779            try:
780                relative = Path(filename).relative_to(package_path)
781            except ValueError:
782                continue
783            # If the path of the file (which is relative to the top of the zip
784            # namespace), relative to the package given when the resource
785            # reader was created, has a parent, then it's a name in a
786            # subdirectory and thus we skip it.
787            parent_name = relative.parent.name
788            if len(parent_name) == 0:
789                yield relative.name
790            elif parent_name not in subdirs_seen:
791                subdirs_seen.add(parent_name)
792                yield parent_name
793