forked from github.com/pypiserver
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:
parent
02ef881387
commit
ab2f170fe9
@ -5,9 +5,9 @@ Changelog
|
|||||||
------------------
|
------------------
|
||||||
- Package-versions parsing:
|
- 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`.
|
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
|
- #102: FIX regression on uploading packages with `+` char in their version
|
||||||
caused by recent bottle-upgrade.
|
caused by recent bottle-upgrade.
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import os
|
|||||||
import zipfile
|
import zipfile
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
from . import core
|
from . import core
|
||||||
|
|
||||||
@ -89,6 +90,11 @@ def root():
|
|||||||
SIMPLE=urljoin(fp, "simple/")
|
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('/')
|
@app.post('/')
|
||||||
@auth("update")
|
@auth("update")
|
||||||
@ -138,7 +144,8 @@ def update():
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
raise HTTPError(400, "Missing 'content' file-field!")
|
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)
|
raise HTTPError(400, "Bad filename: %s" % content.raw_filename)
|
||||||
|
|
||||||
if not config.overwrite and core.exists(packages.root, content.raw_filename):
|
if not config.overwrite and core.exists(packages.root, content.raw_filename):
|
||||||
|
@ -93,8 +93,9 @@ mimetypes.add_type("application/octet-stream", ".egg")
|
|||||||
mimetypes.add_type("application/octet-stream", ".whl")
|
mimetypes.add_type("application/octet-stream", ".whl")
|
||||||
|
|
||||||
|
|
||||||
# --- the following two functions were copied from distribute's pkg_resources module
|
#### Next 2 functions adapted from :mod:`distribute.pkg_resources`.
|
||||||
component_re = re.compile(r'(\d+ | [a-z]+ | \.| -)', re.VERBOSE)
|
#
|
||||||
|
component_re = re.compile(r'(\d+ | [a-z]+ | \.| -)', re.I | re.VERBOSE)
|
||||||
replace = {'pre': 'c', 'preview': 'c', '-': 'final-', 'rc': 'c', 'dev': '@'}.get
|
replace = {'pre': 'c', 'preview': 'c', '-': 'final-', 'rc': 'c', 'dev': '@'}.get
|
||||||
|
|
||||||
|
|
||||||
@ -120,22 +121,23 @@ def parse_version(s):
|
|||||||
parts.pop()
|
parts.pop()
|
||||||
parts.append(part)
|
parts.append(part)
|
||||||
return tuple(parts)
|
return tuple(parts)
|
||||||
|
#
|
||||||
|
#### -- End of distribute's code.
|
||||||
|
|
||||||
# -- end of distribute's code
|
|
||||||
|
|
||||||
_archive_suffix_rx = re.compile(
|
_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)$",
|
r"(\.zip|\.tar\.gz|\.tgz|\.tar\.bz2|-py[23]\.\d-.*|"
|
||||||
re.IGNORECASE)
|
"\.win-amd64-py[23]\.\d\..*|\.win32-py[23]\.\d\..*|\.egg)$",
|
||||||
|
re.I)
|
||||||
wheel_file_re = re.compile(
|
wheel_file_re = re.compile(
|
||||||
r"""^(?P<namever>(?P<name>.+?)-(?P<ver>\d.*?))
|
r"""^(?P<namever>(?P<name>.+?)-(?P<ver>\d.*?))
|
||||||
((-(?P<build>\d.*?))?-(?P<pyver>.+?)-(?P<abi>.+?)-(?P<plat>.+?)
|
((-(?P<build>\d.*?))?-(?P<pyver>.+?)-(?P<abi>.+?)-(?P<plat>.+?)
|
||||||
\.whl|\.dist-info)$""",
|
\.whl|\.dist-info)$""",
|
||||||
re.VERBOSE)
|
re.VERBOSE)
|
||||||
|
_pkgname_re = re.compile(r'-\d+[a-z_.!+]', re.I)
|
||||||
_pkgname_re = re.compile(r'-(?i)v?\d+[\.a-z]')
|
_pkgname_parts_re = re.compile(
|
||||||
_pkgname_parts_re = re.compile(r'[\.\-](?=(?i)cp\d|py\d|macosx|linux|sunos|'
|
r"[\.\-](?=cp\d|py\d|macosx|linux|sunos|solaris|irix|aix|cygwin|win)",
|
||||||
'solaris|irix|aix|cygwin|win)')
|
re.I)
|
||||||
|
|
||||||
|
|
||||||
def _guess_pkgname_and_version_wheel(basename):
|
def _guess_pkgname_and_version_wheel(basename):
|
||||||
@ -180,11 +182,6 @@ def is_allowed_path(path_part):
|
|||||||
p = path_part.replace("\\", "/")
|
p = path_part.replace("\\", "/")
|
||||||
return not (p.startswith(".") or "/." in p)
|
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):
|
class PkgFile(object):
|
||||||
|
|
||||||
@ -210,7 +207,8 @@ class PkgFile(object):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "%s(%s)" % (
|
return "%s(%s)" % (
|
||||||
self.__class__.__name__,
|
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):
|
def hash(self, hash_algo):
|
||||||
if not hasattr(self, '_hash'):
|
if not hasattr(self, '_hash'):
|
||||||
|
@ -323,7 +323,9 @@ def test_upload_badAction(root, testapp):
|
|||||||
assert resp.status == '400 Bad Request'
|
assert resp.status == '400 Bad Request'
|
||||||
assert "Unsupported ':action' field: BAD" in hp.unescape(resp.text)
|
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):
|
def test_upload(package, root, testapp):
|
||||||
resp = testapp.post("/", params={':action': 'file_upload'},
|
resp = testapp.post("/", params={':action': 'file_upload'},
|
||||||
upload_files=[('content', package, b'')])
|
upload_files=[('content', package, b'')])
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
#! /usr/bin/env py.test
|
#! /usr/bin/env py.test
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import pytest
|
|
||||||
from pypiserver import __main__, core
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pypiserver import __main__, core
|
||||||
|
|
||||||
|
|
||||||
## Enable logging to detect any problems with it
|
## 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-py3.2-win32.egg", "greenlet", "0.3.4"),
|
||||||
("greenlet-0.3.4-py2.7-linux-x86_64.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"),
|
("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"),
|
("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"),
|
("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.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-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-2013.02.17.dev123.zip", "package", "2013.02.17.dev123"),
|
||||||
("package-20000101.zip", "package", "20000101"),
|
("package-20000101.zip", "package", "20000101"),
|
||||||
("flup-123-1.0.3.dev-20110405.tar.gz", "flup-123", "1.0.3.dev-20110405"),
|
("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.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-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-2013.02.17.dev123.zip", "package-123", "2013.02.17.dev123"),
|
||||||
("package-123-20000101.zip", "package-123", "20000101"),
|
("package-123-20000101.zip", "package-123", "20000101"),
|
||||||
("pyelasticsearch-0.5-brainbot-1-20130712.zip", "pyelasticsearch", "0.5-brainbot-1-20130712"),
|
("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.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.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"),
|
("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
|
||||||
("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', ''),
|
||||||
("a-%-package-1.0", None, None),
|
("foo/pkg.zip", 'pkg', ''),
|
||||||
("some/pkg-1.0", None, None),
|
("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)
|
@pytest.mark.parametrize(("filename", "pkgname", "version"), files)
|
||||||
def test_guess_pkgname_and_version(filename, pkgname, version):
|
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(filename) == exp
|
||||||
|
assert core.guess_pkgname_and_version(_capitalize_ext(filename)) == exp
|
||||||
|
|
||||||
|
|
||||||
def test_listdir_bad_name(tmpdir):
|
def test_listdir_bad_name(tmpdir):
|
||||||
|
Loading…
Reference in New Issue
Block a user