Path: blob/main/test/lib/python3.9/site-packages/pip/_internal/utils/unpacking.py
4804 views
"""Utilities related archives.1"""23import logging4import os5import shutil6import stat7import tarfile8import zipfile9from typing import Iterable, List, Optional10from zipfile import ZipInfo1112from pip._internal.exceptions import InstallationError13from pip._internal.utils.filetypes import (14BZ2_EXTENSIONS,15TAR_EXTENSIONS,16XZ_EXTENSIONS,17ZIP_EXTENSIONS,18)19from pip._internal.utils.misc import ensure_dir2021logger = logging.getLogger(__name__)222324SUPPORTED_EXTENSIONS = ZIP_EXTENSIONS + TAR_EXTENSIONS2526try:27import bz2 # noqa2829SUPPORTED_EXTENSIONS += BZ2_EXTENSIONS30except ImportError:31logger.debug("bz2 module is not available")3233try:34# Only for Python 3.3+35import lzma # noqa3637SUPPORTED_EXTENSIONS += XZ_EXTENSIONS38except ImportError:39logger.debug("lzma module is not available")404142def current_umask() -> int:43"""Get the current umask which involves having to set it temporarily."""44mask = os.umask(0)45os.umask(mask)46return mask474849def split_leading_dir(path: str) -> List[str]:50path = path.lstrip("/").lstrip("\\")51if "/" in path and (52("\\" in path and path.find("/") < path.find("\\")) or "\\" not in path53):54return path.split("/", 1)55elif "\\" in path:56return path.split("\\", 1)57else:58return [path, ""]596061def has_leading_dir(paths: Iterable[str]) -> bool:62"""Returns true if all the paths have the same leading path name63(i.e., everything is in one subdirectory in an archive)"""64common_prefix = None65for path in paths:66prefix, rest = split_leading_dir(path)67if not prefix:68return False69elif common_prefix is None:70common_prefix = prefix71elif prefix != common_prefix:72return False73return True747576def is_within_directory(directory: str, target: str) -> bool:77"""78Return true if the absolute path of target is within the directory79"""80abs_directory = os.path.abspath(directory)81abs_target = os.path.abspath(target)8283prefix = os.path.commonprefix([abs_directory, abs_target])84return prefix == abs_directory858687def set_extracted_file_to_default_mode_plus_executable(path: str) -> None:88"""89Make file present at path have execute for user/group/world90(chmod +x) is no-op on windows per python docs91"""92os.chmod(path, (0o777 & ~current_umask() | 0o111))939495def zip_item_is_executable(info: ZipInfo) -> bool:96mode = info.external_attr >> 1697# if mode and regular file and any execute permissions for98# user/group/world?99return bool(mode and stat.S_ISREG(mode) and mode & 0o111)100101102def unzip_file(filename: str, location: str, flatten: bool = True) -> None:103"""104Unzip the file (with path `filename`) to the destination `location`. All105files are written based on system defaults and umask (i.e. permissions are106not preserved), except that regular file members with any execute107permissions (user, group, or world) have "chmod +x" applied after being108written. Note that for windows, any execute changes using os.chmod are109no-ops per the python docs.110"""111ensure_dir(location)112zipfp = open(filename, "rb")113try:114zip = zipfile.ZipFile(zipfp, allowZip64=True)115leading = has_leading_dir(zip.namelist()) and flatten116for info in zip.infolist():117name = info.filename118fn = name119if leading:120fn = split_leading_dir(name)[1]121fn = os.path.join(location, fn)122dir = os.path.dirname(fn)123if not is_within_directory(location, fn):124message = (125"The zip file ({}) has a file ({}) trying to install "126"outside target directory ({})"127)128raise InstallationError(message.format(filename, fn, location))129if fn.endswith("/") or fn.endswith("\\"):130# A directory131ensure_dir(fn)132else:133ensure_dir(dir)134# Don't use read() to avoid allocating an arbitrarily large135# chunk of memory for the file's content136fp = zip.open(name)137try:138with open(fn, "wb") as destfp:139shutil.copyfileobj(fp, destfp)140finally:141fp.close()142if zip_item_is_executable(info):143set_extracted_file_to_default_mode_plus_executable(fn)144finally:145zipfp.close()146147148def untar_file(filename: str, location: str) -> None:149"""150Untar the file (with path `filename`) to the destination `location`.151All files are written based on system defaults and umask (i.e. permissions152are not preserved), except that regular file members with any execute153permissions (user, group, or world) have "chmod +x" applied after being154written. Note that for windows, any execute changes using os.chmod are155no-ops per the python docs.156"""157ensure_dir(location)158if filename.lower().endswith(".gz") or filename.lower().endswith(".tgz"):159mode = "r:gz"160elif filename.lower().endswith(BZ2_EXTENSIONS):161mode = "r:bz2"162elif filename.lower().endswith(XZ_EXTENSIONS):163mode = "r:xz"164elif filename.lower().endswith(".tar"):165mode = "r"166else:167logger.warning(168"Cannot determine compression type for file %s",169filename,170)171mode = "r:*"172tar = tarfile.open(filename, mode, encoding="utf-8")173try:174leading = has_leading_dir([member.name for member in tar.getmembers()])175for member in tar.getmembers():176fn = member.name177if leading:178fn = split_leading_dir(fn)[1]179path = os.path.join(location, fn)180if not is_within_directory(location, path):181message = (182"The tar file ({}) has a file ({}) trying to install "183"outside target directory ({})"184)185raise InstallationError(message.format(filename, path, location))186if member.isdir():187ensure_dir(path)188elif member.issym():189try:190tar._extract_member(member, path)191except Exception as exc:192# Some corrupt tar files seem to produce this193# (specifically bad symlinks)194logger.warning(195"In the tar file %s the member %s is invalid: %s",196filename,197member.name,198exc,199)200continue201else:202try:203fp = tar.extractfile(member)204except (KeyError, AttributeError) as exc:205# Some corrupt tar files seem to produce this206# (specifically bad symlinks)207logger.warning(208"In the tar file %s the member %s is invalid: %s",209filename,210member.name,211exc,212)213continue214ensure_dir(os.path.dirname(path))215assert fp is not None216with open(path, "wb") as destfp:217shutil.copyfileobj(fp, destfp)218fp.close()219# Update the timestamp (useful for cython compiled files)220tar.utime(member, path)221# member have any execute permissions for user/group/world?222if member.mode & 0o111:223set_extracted_file_to_default_mode_plus_executable(path)224finally:225tar.close()226227228def unpack_file(229filename: str,230location: str,231content_type: Optional[str] = None,232) -> None:233filename = os.path.realpath(filename)234if (235content_type == "application/zip"236or filename.lower().endswith(ZIP_EXTENSIONS)237or zipfile.is_zipfile(filename)238):239unzip_file(filename, location, flatten=not filename.endswith(".whl"))240elif (241content_type == "application/x-gzip"242or tarfile.is_tarfile(filename)243or filename.lower().endswith(TAR_EXTENSIONS + BZ2_EXTENSIONS + XZ_EXTENSIONS)244):245untar_file(filename, location)246else:247# FIXME: handle?248# FIXME: magic signatures?249logger.critical(250"Cannot unpack file %s (downloaded from %s, content-type: %s); "251"cannot detect archive format",252filename,253location,254content_type,255)256raise InstallationError(f"Cannot determine archive format of {location}")257258259