Skip to content

Commit 100e9a6

Browse files
authored
Merge pull request #4951
More fully sanitize the filename in PackageIndex._download_url Closes #4951
2 parents 4e1e893 + 8faf1d7 commit 100e9a6

File tree

2 files changed

+54
-11
lines changed

2 files changed

+54
-11
lines changed

newsfragments/4946.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
More fully sanitized the filename in PackageIndex._download.

setuptools/package_index.py

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -807,21 +807,63 @@ def open_url(self, url, warning=None): # noqa: C901 # is too complex (12)
807807
else:
808808
raise DistutilsError(f"Download error for {url}: {v}") from v
809809

810-
def _download_url(self, url, tmpdir):
811-
# Determine download filename
812-
#
810+
@staticmethod
811+
def _sanitize(name):
812+
r"""
813+
Replace unsafe path directives with underscores.
814+
815+
>>> san = PackageIndex._sanitize
816+
>>> san('/home/user/.ssh/authorized_keys')
817+
'_home_user_.ssh_authorized_keys'
818+
>>> san('..\\foo\\bing')
819+
'__foo_bing'
820+
>>> san('D:bar')
821+
'D_bar'
822+
>>> san('C:\\bar')
823+
'C__bar'
824+
>>> san('foo..bar')
825+
'foo..bar'
826+
>>> san('D:../foo')
827+
'D___foo'
828+
"""
829+
pattern = '|'.join((
830+
# drive letters
831+
r':',
832+
# path separators
833+
r'[/\\]',
834+
# parent dirs
835+
r'(?:(?<=([/\\]|:))\.\.(?=[/\\]|$))|(?:^\.\.(?=[/\\]|$))',
836+
))
837+
return re.sub(pattern, r'_', name)
838+
839+
@classmethod
840+
def _resolve_download_filename(cls, url, tmpdir):
841+
"""
842+
>>> import pathlib
843+
>>> du = PackageIndex._resolve_download_filename
844+
>>> root = getfixture('tmp_path')
845+
>>> url = 'https://files.pythonhosted.org/packages/a9/5a/0db.../setuptools-78.1.0.tar.gz'
846+
>>> str(pathlib.Path(du(url, root)).relative_to(root))
847+
'setuptools-78.1.0.tar.gz'
848+
"""
813849
name, _fragment = egg_info_for_url(url)
814-
if name:
815-
while '..' in name:
816-
name = name.replace('..', '.').replace('\\', '_')
817-
else:
818-
name = "__downloaded__" # default if URL has no path contents
850+
name = cls._sanitize(
851+
name
852+
or
853+
# default if URL has no path contents
854+
'__downloaded__'
855+
)
819856

820-
if name.endswith('.egg.zip'):
821-
name = name[:-4] # strip the extra .zip before download
857+
# strip any extra .zip before download
858+
name = re.sub(r'\.egg\.zip$', '.egg', name)
822859

823-
filename = os.path.join(tmpdir, name)
860+
return os.path.join(tmpdir, name)
824861

862+
def _download_url(self, url, tmpdir):
863+
"""
864+
Determine the download filename.
865+
"""
866+
filename = self._resolve_download_filename(url, tmpdir)
825867
return self._download_vcs(url, filename) or self._download_other(url, filename)
826868

827869
@staticmethod

0 commit comments

Comments
 (0)