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