diff --git a/setuptools/package_index.py b/setuptools/package_index.py index 1a6abebcda..3500c2d86f 100644 --- a/setuptools/package_index.py +++ b/setuptools/package_index.py @@ -807,21 +807,63 @@ def open_url(self, url, warning=None): # noqa: C901 # is too complex (12) else: raise DistutilsError(f"Download error for {url}: {v}") from v - def _download_url(self, url, tmpdir): - # Determine download filename - # + @staticmethod + def _sanitize(name): + r""" + Replace unsafe path directives with underscores. + + >>> san = PackageIndex._sanitize + >>> san('/home/user/.ssh/authorized_keys') + '_home_user_.ssh_authorized_keys' + >>> san('..\\foo\\bing') + '__foo_bing' + >>> san('D:bar') + 'D_bar' + >>> san('C:\\bar') + 'C__bar' + >>> san('foo..bar') + 'foo..bar' + >>> san('D:../foo') + 'D___foo' + """ + pattern = '|'.join(( + # drive letters + r':', + # path separators + r'[/\\]', + # parent dirs + r'(?:(?<=([/\\]|:))\.\.(?=[/\\]|$))|(?:^\.\.(?=[/\\]|$))', + )) + return re.sub(pattern, r'_', name) + + @classmethod + def _resolve_download_filename(cls, url, tmpdir): + """ + >>> import pathlib + >>> du = PackageIndex._resolve_download_filename + >>> root = getfixture('tmp_path') + >>> url = 'https://files.pythonhosted.org/packages/a9/5a/0db.../setuptools-78.1.0.tar.gz' + >>> str(pathlib.Path(du(url, root)).relative_to(root)) + 'setuptools-78.1.0.tar.gz' + """ name, _fragment = egg_info_for_url(url) - if name: - while '..' in name: - name = name.replace('..', '.').replace('\\', '_') - else: - name = "__downloaded__" # default if URL has no path contents + name = cls._sanitize( + name + or + # default if URL has no path contents + '__downloaded__' + ) - if name.endswith('.egg.zip'): - name = name[:-4] # strip the extra .zip before download + # strip any extra .zip before download + name = re.sub(r'\.egg\.zip$', '.egg', name) - filename = os.path.join(tmpdir, name) + return os.path.join(tmpdir, name) + def _download_url(self, url, tmpdir): + """ + Determine the download filename. + """ + filename = self._resolve_download_filename(url, tmpdir) return self._download_vcs(url, filename) or self._download_other(url, filename) @staticmethod