Path: blob/master/venv/Lib/site-packages/pip/_internal/utils/unpacking.py
811 views
"""Utilities related archives.1"""23# The following comment should be removed at some point in the future.4# mypy: strict-optional=False5# mypy: disallow-untyped-defs=False67from __future__ import absolute_import89import logging10import os11import shutil12import stat13import tarfile14import zipfile1516from pip._internal.exceptions import InstallationError17from pip._internal.utils.filetypes import (18BZ2_EXTENSIONS,19TAR_EXTENSIONS,20XZ_EXTENSIONS,21ZIP_EXTENSIONS,22)23from pip._internal.utils.misc import ensure_dir24from pip._internal.utils.typing import MYPY_CHECK_RUNNING2526if MYPY_CHECK_RUNNING:27from typing import Iterable, List, Optional, Text, Union282930logger = logging.getLogger(__name__)313233SUPPORTED_EXTENSIONS = ZIP_EXTENSIONS + TAR_EXTENSIONS3435try:36import bz2 # noqa37SUPPORTED_EXTENSIONS += BZ2_EXTENSIONS38except ImportError:39logger.debug('bz2 module is not available')4041try:42# Only for Python 3.3+43import lzma # noqa44SUPPORTED_EXTENSIONS += XZ_EXTENSIONS45except ImportError:46logger.debug('lzma module is not available')474849def current_umask():50"""Get the current umask which involves having to set it temporarily."""51mask = os.umask(0)52os.umask(mask)53return mask545556def split_leading_dir(path):57# type: (Union[str, Text]) -> List[Union[str, Text]]58path = path.lstrip('/').lstrip('\\')59if (60'/' in path and (61('\\' in path and path.find('/') < path.find('\\')) or62'\\' not in path63)64):65return path.split('/', 1)66elif '\\' in path:67return path.split('\\', 1)68else:69return [path, '']707172def has_leading_dir(paths):73# type: (Iterable[Union[str, Text]]) -> bool74"""Returns true if all the paths have the same leading path name75(i.e., everything is in one subdirectory in an archive)"""76common_prefix = None77for path in paths:78prefix, rest = split_leading_dir(path)79if not prefix:80return False81elif common_prefix is None:82common_prefix = prefix83elif prefix != common_prefix:84return False85return True868788def is_within_directory(directory, target):89# type: ((Union[str, Text]), (Union[str, Text])) -> bool90"""91Return true if the absolute path of target is within the directory92"""93abs_directory = os.path.abspath(directory)94abs_target = os.path.abspath(target)9596prefix = os.path.commonprefix([abs_directory, abs_target])97return prefix == abs_directory9899100def unzip_file(filename, location, flatten=True):101# type: (str, str, bool) -> None102"""103Unzip the file (with path `filename`) to the destination `location`. All104files are written based on system defaults and umask (i.e. permissions are105not preserved), except that regular file members with any execute106permissions (user, group, or world) have "chmod +x" applied after being107written. Note that for windows, any execute changes using os.chmod are108no-ops per the python docs.109"""110ensure_dir(location)111zipfp = open(filename, 'rb')112try:113zip = zipfile.ZipFile(zipfp, allowZip64=True)114leading = has_leading_dir(zip.namelist()) and flatten115for info in zip.infolist():116name = info.filename117fn = name118if leading:119fn = split_leading_dir(name)[1]120fn = os.path.join(location, fn)121dir = os.path.dirname(fn)122if not is_within_directory(location, fn):123message = (124'The zip file ({}) has a file ({}) trying to install '125'outside target directory ({})'126)127raise InstallationError(message.format(filename, fn, location))128if fn.endswith('/') or fn.endswith('\\'):129# A directory130ensure_dir(fn)131else:132ensure_dir(dir)133# Don't use read() to avoid allocating an arbitrarily large134# chunk of memory for the file's content135fp = zip.open(name)136try:137with open(fn, 'wb') as destfp:138shutil.copyfileobj(fp, destfp)139finally:140fp.close()141mode = info.external_attr >> 16142# if mode and regular file and any execute permissions for143# user/group/world?144if mode and stat.S_ISREG(mode) and mode & 0o111:145# make dest file have execute for user/group/world146# (chmod +x) no-op on windows per python docs147os.chmod(fn, (0o777 - current_umask() | 0o111))148finally:149zipfp.close()150151152def untar_file(filename, location):153# type: (str, str) -> None154"""155Untar the file (with path `filename`) to the destination `location`.156All files are written based on system defaults and umask (i.e. permissions157are not preserved), except that regular file members with any execute158permissions (user, group, or world) have "chmod +x" applied after being159written. Note that for windows, any execute changes using os.chmod are160no-ops per the python docs.161"""162ensure_dir(location)163if filename.lower().endswith('.gz') or filename.lower().endswith('.tgz'):164mode = 'r:gz'165elif filename.lower().endswith(BZ2_EXTENSIONS):166mode = 'r:bz2'167elif filename.lower().endswith(XZ_EXTENSIONS):168mode = 'r:xz'169elif filename.lower().endswith('.tar'):170mode = 'r'171else:172logger.warning(173'Cannot determine compression type for file %s', filename,174)175mode = 'r:*'176tar = tarfile.open(filename, mode)177try:178leading = has_leading_dir([179member.name for member in tar.getmembers()180])181for member in tar.getmembers():182fn = member.name183if leading:184# https://github.com/python/mypy/issues/1174185fn = split_leading_dir(fn)[1] # type: ignore186path = os.path.join(location, fn)187if not is_within_directory(location, path):188message = (189'The tar file ({}) has a file ({}) trying to install '190'outside target directory ({})'191)192raise InstallationError(193message.format(filename, path, location)194)195if member.isdir():196ensure_dir(path)197elif member.issym():198try:199# https://github.com/python/typeshed/issues/2673200tar._extract_member(member, path) # type: ignore201except Exception as exc:202# Some corrupt tar files seem to produce this203# (specifically bad symlinks)204logger.warning(205'In the tar file %s the member %s is invalid: %s',206filename, member.name, exc,207)208continue209else:210try:211fp = tar.extractfile(member)212except (KeyError, AttributeError) as exc:213# Some corrupt tar files seem to produce this214# (specifically bad symlinks)215logger.warning(216'In the tar file %s the member %s is invalid: %s',217filename, member.name, exc,218)219continue220ensure_dir(os.path.dirname(path))221with open(path, 'wb') as destfp:222shutil.copyfileobj(fp, destfp)223fp.close()224# Update the timestamp (useful for cython compiled files)225# https://github.com/python/typeshed/issues/2673226tar.utime(member, path) # type: ignore227# member have any execute permissions for user/group/world?228if member.mode & 0o111:229# make dest file have execute for user/group/world230# no-op on windows per python docs231os.chmod(path, (0o777 - current_umask() | 0o111))232finally:233tar.close()234235236def unpack_file(237filename, # type: str238location, # type: str239content_type=None, # type: Optional[str]240):241# type: (...) -> None242filename = os.path.realpath(filename)243if (244content_type == 'application/zip' or245filename.lower().endswith(ZIP_EXTENSIONS) or246zipfile.is_zipfile(filename)247):248unzip_file(249filename,250location,251flatten=not filename.endswith('.whl')252)253elif (254content_type == 'application/x-gzip' or255tarfile.is_tarfile(filename) or256filename.lower().endswith(257TAR_EXTENSIONS + BZ2_EXTENSIONS + XZ_EXTENSIONS258)259):260untar_file(filename, location)261else:262# FIXME: handle?263# FIXME: magic signatures?264logger.critical(265'Cannot unpack file %s (downloaded from %s, content-type: %s); '266'cannot detect archive format',267filename, location, content_type,268)269raise InstallationError(270'Cannot determine archive format of {}'.format(location)271)272273274