codekingpro/portable-devtools
114k
1from __future__ import annotations2 3import csv4import hashlib5import os.path6import re7import stat8import time9from io import StringIO, TextIOWrapper10from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo11 12from wheel.cli import WheelError13from wheel.util import log, urlsafe_b64decode, urlsafe_b64encode14 15# Non-greedy matching of an optional build number may be too clever (more16# invalid wheel filenames will match). Separate regex for .dist-info?17WHEEL_INFO_RE = re.compile(18 r"""^(?P<namever>(?P<name>[^\s-]+?)-(?P<ver>[^\s-]+?))(-(?P<build>\d[^\s-]*))?19 -(?P<pyver>[^\s-]+?)-(?P<abi>[^\s-]+?)-(?P<plat>\S+)\.whl$""",20 re.VERBOSE,21)22MINIMUM_TIMESTAMP = 315532800 # 1980-01-01 00:00:00 UTC23 24 25def get_zipinfo_datetime(timestamp=None):26 # Some applications need reproducible .whl files, but they can't do this without27 # forcing the timestamp of the individual ZipInfo objects. See issue #143.28 timestamp = int(os.environ.get("SOURCE_DATE_EPOCH", timestamp or time.time()))29 timestamp = max(timestamp, MINIMUM_TIMESTAMP)30 return time.gmtime(timestamp)[0:6]31 32 33class WheelFile(ZipFile):34 """A ZipFile derivative class that also reads SHA-256 hashes from35 .dist-info/RECORD and checks any read files against those.36 """37 38 _default_algorithm = hashlib.sha25639 40 def __init__(self, file, mode="r", compression=ZIP_DEFLATED):41 basename = os.path.basename(file)42 self.parsed_filename = WHEEL_INFO_RE.match(basename)43 if not basename.endswith(".whl") or self.parsed_filename is None:44 raise WheelError(f"Bad wheel filename {basename!r}")45 46 ZipFile.__init__(self, file, mode, compression=compression, allowZip64=True)47 48 self.dist_info_path = "{}.dist-info".format(49 self.parsed_filename.group("namever")50 )51 self.record_path = self.dist_info_path + "/RECORD"52 self._file_hashes = {}53 self._file_sizes = {}54 if mode == "r":55 # Ignore RECORD and any embedded wheel signatures56 self._file_hashes[self.record_path] = None, None57 self._file_hashes[self.record_path + ".jws"] = None, None58 self._file_hashes[self.record_path + ".p7s"] = None, None59 60 # Fill in the expected hashes by reading them from RECORD61 try:62 record = self.open(self.record_path)63 except KeyError:64 raise WheelError(f"Missing {self.record_path} file") from None65 66 with record:67 for line in csv.reader(68 TextIOWrapper(record, newline="", encoding="utf-8")69 ):70 path, hash_sum, size = line71 if not hash_sum:72 continue73 74 algorithm, hash_sum = hash_sum.split("=")75 try:76 hashlib.new(algorithm)77 except ValueError:78 raise WheelError(79 f"Unsupported hash algorithm: {algorithm}"80 ) from None81 82 if algorithm.lower() in {"md5", "sha1"}:83 raise WheelError(84 f"Weak hash algorithm ({algorithm}) is not permitted by "85 f"PEP 427"86 )87 88 self._file_hashes[path] = (89 algorithm,90 urlsafe_b64decode(hash_sum.encode("ascii")),91 )92 93 def open(self, name_or_info, mode="r", pwd=None):94 def _update_crc(newdata):95 eof = ef._eof96 update_crc_orig(newdata)97 running_hash.update(newdata)98 if eof and running_hash.digest() != expected_hash:99 raise WheelError(f"Hash mismatch for file '{ef_name}'")100 101 ef_name = (102 name_or_info.filename if isinstance(name_or_info, ZipInfo) else name_or_info103 )104 if (105 mode == "r"106 and not ef_name.endswith("/")107 and ef_name not in self._file_hashes108 ):109 raise WheelError(f"No hash found for file '{ef_name}'")110 111 ef = ZipFile.open(self, name_or_info, mode, pwd)112 if mode == "r" and not ef_name.endswith("/"):113 algorithm, expected_hash = self._file_hashes[ef_name]114 if expected_hash is not None:115 # Monkey patch the _update_crc method to also check for the hash from116 # RECORD117 running_hash = hashlib.new(algorithm)118 update_crc_orig, ef._update_crc = ef._update_crc, _update_crc119 120 return ef121 122 def write_files(self, base_dir):123 log.info(f"creating '{self.filename}' and adding '{base_dir}' to it")124 deferred = []125 for root, dirnames, filenames in os.walk(base_dir):126 # Sort the directory names so that `os.walk` will walk them in a127 # defined order on the next iteration.128 dirnames.sort()129 for name in sorted(filenames):130 path = os.path.normpath(os.path.join(root, name))131 if os.path.isfile(path):132 arcname = os.path.relpath(path, base_dir).replace(os.path.sep, "/")133 if arcname == self.record_path:134 pass135 elif root.endswith(".dist-info"):136 deferred.append((path, arcname))137 else:138 self.write(path, arcname)139 140 deferred.sort()141 for path, arcname in deferred:142 self.write(path, arcname)143 144 def write(self, filename, arcname=None, compress_type=None):145 with open(filename, "rb") as f:146 st = os.fstat(f.fileno())147 data = f.read()148 149 zinfo = ZipInfo(150 arcname or filename, date_time=get_zipinfo_datetime(st.st_mtime)151 )152 zinfo.external_attr = (stat.S_IMODE(st.st_mode) | stat.S_IFMT(st.st_mode)) << 16153 zinfo.compress_type = compress_type or self.compression154 self.writestr(zinfo, data, compress_type)155 156 def writestr(self, zinfo_or_arcname, data, compress_type=None):157 if isinstance(zinfo_or_arcname, str):158 zinfo_or_arcname = ZipInfo(159 zinfo_or_arcname, date_time=get_zipinfo_datetime()160 )161 zinfo_or_arcname.compress_type = self.compression162 zinfo_or_arcname.external_attr = (0o664 | stat.S_IFREG) << 16163 164 if isinstance(data, str):165 data = data.encode("utf-8")166 167 ZipFile.writestr(self, zinfo_or_arcname, data, compress_type)168 fname = (169 zinfo_or_arcname.filename170 if isinstance(zinfo_or_arcname, ZipInfo)171 else zinfo_or_arcname172 )173 log.info(f"adding '{fname}'")174 if fname != self.record_path:175 hash_ = self._default_algorithm(data)176 self._file_hashes[fname] = (177 hash_.name,178 urlsafe_b64encode(hash_.digest()).decode("ascii"),179 )180 self._file_sizes[fname] = len(data)181 182 def close(self):183 # Write RECORD184 if self.fp is not None and self.mode == "w" and self._file_hashes:185 data = StringIO()186 writer = csv.writer(data, delimiter=",", quotechar='"', lineterminator="\n")187 writer.writerows(188 (189 (fname, algorithm + "=" + hash_, self._file_sizes[fname])190 for fname, (algorithm, hash_) in self._file_hashes.items()191 )192 )193 writer.writerow((format(self.record_path), "", ""))194 self.writestr(self.record_path, data.getvalue())195 196 ZipFile.close(self)197 