diff --git a/CHANGES.rst b/CHANGES.rst index e696caa..c2d76f4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,9 +5,9 @@ Changelog ------------------ - Package-versions parsing: - - TODO: #104: Stopped parsing invalid package-versions prefixed with `v`; they are + - #104: Stopped parsing invalid package-versions prefixed with `v`; they are invalid according to :pep-reference:`0440`. - - TODO: Support versions with epochs separated by `!` like `package-1!1.1.0`. + - Support versions with epochs separated by `!` like `package-1!1.1.0`. - #102: FIX regression on uploading packages with `+` char in their version caused by recent bottle-upgrade. diff --git a/pypiserver/_app.py b/pypiserver/_app.py index cfae7ab..b690575 100644 --- a/pypiserver/_app.py +++ b/pypiserver/_app.py @@ -2,6 +2,7 @@ import os import zipfile import mimetypes import logging +import re from . import core @@ -89,6 +90,11 @@ def root(): SIMPLE=urljoin(fp, "simple/") ) +_bottle_upload_filename_re = re.compile(r'^[a-z0-9_.!+-]+$', re.I) +def is_valid_pkg_filename(fname): + """See https://github.com/pypiserver/pypiserver/issues/102""" + return _bottle_upload_filename_re.match(fname) is not None + @app.post('/') @auth("update") @@ -138,7 +144,8 @@ def update(): except KeyError: raise HTTPError(400, "Missing 'content' file-field!") - if not core.is_valid_pkg_filename(content.raw_filename): + if (not is_valid_pkg_filename(content.raw_filename) or + core.guess_pkgname_and_version(content.raw_filename) is None): raise HTTPError(400, "Bad filename: %s" % content.raw_filename) if not config.overwrite and core.exists(packages.root, content.raw_filename): diff --git a/pypiserver/core.py b/pypiserver/core.py index e121f05..a7078dd 100644 --- a/pypiserver/core.py +++ b/pypiserver/core.py @@ -93,8 +93,9 @@ mimetypes.add_type("application/octet-stream", ".egg") mimetypes.add_type("application/octet-stream", ".whl") -# --- the following two functions were copied from distribute's pkg_resources module -component_re = re.compile(r'(\d+ | [a-z]+ | \.| -)', re.VERBOSE) +#### Next 2 functions adapted from :mod:`distribute.pkg_resources`. +# +component_re = re.compile(r'(\d+ | [a-z]+ | \.| -)', re.I | re.VERBOSE) replace = {'pre': 'c', 'preview': 'c', '-': 'final-', 'rc': 'c', 'dev': '@'}.get @@ -120,22 +121,23 @@ def parse_version(s): parts.pop() parts.append(part) return tuple(parts) +# +#### -- End of distribute's code. -# -- end of distribute's code _archive_suffix_rx = re.compile( - r"(\.zip|\.tar\.gz|\.tgz|\.tar\.bz2|-py[23]\.\d-.*|\.win-amd64-py[23]\.\d\..*|\.win32-py[23]\.\d\..*|\.egg)$", - re.IGNORECASE) - + r"(\.zip|\.tar\.gz|\.tgz|\.tar\.bz2|-py[23]\.\d-.*|" + "\.win-amd64-py[23]\.\d\..*|\.win32-py[23]\.\d\..*|\.egg)$", + re.I) wheel_file_re = re.compile( r"""^(?P(?P.+?)-(?P\d.*?)) ((-(?P\d.*?))?-(?P.+?)-(?P.+?)-(?P.+?) \.whl|\.dist-info)$""", re.VERBOSE) - -_pkgname_re = re.compile(r'-(?i)v?\d+[\.a-z]') -_pkgname_parts_re = re.compile(r'[\.\-](?=(?i)cp\d|py\d|macosx|linux|sunos|' - 'solaris|irix|aix|cygwin|win)') +_pkgname_re = re.compile(r'-\d+[a-z_.!+]', re.I) +_pkgname_parts_re = re.compile( + r"[\.\-](?=cp\d|py\d|macosx|linux|sunos|solaris|irix|aix|cygwin|win)", + re.I) def _guess_pkgname_and_version_wheel(basename): @@ -180,11 +182,6 @@ def is_allowed_path(path_part): p = path_part.replace("\\", "/") return not (p.startswith(".") or "/." in p) -_bottle_upload_filename_re = re.compile(r'^[a-z0-9_.!+-]+$', re.I) - -def is_valid_pkg_filename(fname): - return _bottle_upload_filename_re.match(fname) is not None - class PkgFile(object): @@ -210,7 +207,8 @@ class PkgFile(object): def __repr__(self): return "%s(%s)" % ( self.__class__.__name__, - ", ".join(["%s=%r" % (k, getattr(self, k, v)) for k in sorted(self.__slots__)])) + ", ".join(["%s=%r" % (k, getattr(self, k)) + for k in sorted(self.__slots__)])) def hash(self, hash_algo): if not hasattr(self, '_hash'): diff --git a/tests/test_app.py b/tests/test_app.py index d065954..9e4e3e9 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -323,7 +323,9 @@ def test_upload_badAction(root, testapp): assert resp.status == '400 Bad Request' assert "Unsupported ':action' field: BAD" in hp.unescape(resp.text) -@pytest.mark.parametrize(("package"), [f[0] for f in test_core.files if f[1]]) +@pytest.mark.parametrize(("package"), [f[0] + for f in test_core.files + if f[1] and '/' not in f[0]]) def test_upload(package, root, testapp): resp = testapp.post("/", params={':action': 'file_upload'}, upload_files=[('content', package, b'')]) diff --git a/tests/test_core.py b/tests/test_core.py index 24b3d83..a95f1da 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,9 +1,13 @@ #! /usr/bin/env py.test # -*- coding: utf-8 -*- -import pytest -from pypiserver import __main__, core import logging +import os + +import pytest + +from pypiserver import __main__, core + ## Enable logging to detect any problems with it ## @@ -22,18 +26,19 @@ files = [ ("greenlet-0.3.4-py3.2-win32.egg", "greenlet", "0.3.4"), ("greenlet-0.3.4-py2.7-linux-x86_64.egg", "greenlet", "0.3.4"), ("pep8-0.6.0.zip", "pep8", "0.6.0"), - ("ABC12-34_V1X-1.2.3.zip", "ABC12-34_V1X", "1.2.3"), + ("ABC12-34_V1X-1.2.3.zip", "ABC12", "34_V1X-1.2.3"), ("A100-200-XYZ-1.2.3.zip", "A100-200-XYZ", "1.2.3"), ("flup-1.0.3.dev-20110405.tar.gz", "flup", "1.0.3.dev-20110405"), ("package-1.0.0-alpha.1.zip", "package", "1.0.0-alpha.1"), ("package-1.3.7+build.11.e0f985a.zip", "package", "1.3.7+build.11.e0f985a"), - ("package-v1.8.1.301.ga0df26f.zip", "package", "v1.8.1.301.ga0df26f"), + ("package-v1-8.1.301.ga0df26f.zip", "package-v1", "8.1.301.ga0df26f"), + ("package-v1.1-8.1.301.ga0df26f.zip", "package-v1.1", "8.1.301.ga0df26f"), ("package-2013.02.17.dev123.zip", "package", "2013.02.17.dev123"), ("package-20000101.zip", "package", "20000101"), ("flup-123-1.0.3.dev-20110405.tar.gz", "flup-123", "1.0.3.dev-20110405"), ("package-123-1.0.0-alpha.1.zip", "package-123", "1.0.0-alpha.1"), ("package-123-1.3.7+build.11.e0f985a.zip", "package-123", "1.3.7+build.11.e0f985a"), - ("package-123-v1.8.1.301.ga0df26f.zip", "package-123", "v1.8.1.301.ga0df26f"), + ("package-123-v1.1_3-8.1.zip", "package-123-v1.1_3", "8.1"), ("package-123-2013.02.17.dev123.zip", "package-123", "2013.02.17.dev123"), ("package-123-20000101.zip", "package-123", "20000101"), ("pyelasticsearch-0.5-brainbot-1-20130712.zip", "pyelasticsearch", "0.5-brainbot-1-20130712"), @@ -44,21 +49,24 @@ files = [ ("package-name-0.0.1.dev0.linux-x86_64.tar.gz", "package-name", "0.0.1.dev0"), ("package-name-0.0.1.dev0.macosx-10.10-intel.tar.gz", "package-name", "0.0.1.dev0"), ("package-name-0.0.1.alpha.1.win-amd64-py3.2.exe", "package-name", "0.0.1.alpha.1"), - ("pkg-3!1.0-0.1.tgz", 'pkg-3!1.0', '0.1'), # TO BE FIXED - ("pkg-3!1+.0-0.1.tgz", 'pkg-3!1+.0', '0.1'), # TO BE FIXED - - ("a-%-package-1.0", None, None), - ("some/pkg-1.0", None, None), + ("pkg-3!1.0-0.1.tgz", 'pkg', '3!1.0-0.1'), # TO BE FIXED + ("pkg-3!1+.0-0.1.tgz", 'pkg', '3!1+.0-0.1'), # TO BE FIXED + ("pkg.zip", 'pkg', ''), + ("foo/pkg.zip", 'pkg', ''), + ("foo/pkg-1b.zip", 'pkg', '1b'), ] +def _capitalize_ext(fpath): + f, e = os.path.splitext(fpath) + if e != '.whl': + e = e.upper() + return f + e @pytest.mark.parametrize(("filename", "pkgname", "version"), files) def test_guess_pkgname_and_version(filename, pkgname, version): - if pkgname is None: - exp = None - else: - exp = (pkgname, version) + exp = (pkgname, version) assert core.guess_pkgname_and_version(filename) == exp + assert core.guess_pkgname_and_version(_capitalize_ext(filename)) == exp def test_listdir_bad_name(tmpdir):