Rework package parsing.

+ FIX #104: Stopped parsing invalid package-versions prefixed with `v`;
  they are invalid according to :pep-reference:`0440`.
+ Also support versions with epochs separated by `!` like `package-1!1.1.0`.
+ Move bottle-filename check on app module.
This commit is contained in:
Kostis Anagnostopoulos 2016-01-19 13:35:41 +01:00
parent 02ef881387
commit ab2f170fe9
5 changed files with 49 additions and 34 deletions

View File

@ -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.

View File

@ -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):

View File

@ -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<namever>(?P<name>.+?)-(?P<ver>\d.*?))
((-(?P<build>\d.*?))?-(?P<pyver>.+?)-(?P<abi>.+?)-(?P<plat>.+?)
\.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'):

View File

@ -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'')])

View File

@ -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):