Skip to content

Commit 3a92170

Browse files
committed
Fix erroneous non-ASCII in local version (#469)
* Fix validation in the packaging.version.Version's constructor * Fix validation in the packaging.specifiers.Specifier's constructor * Fix docs of packaging.version.VERSION_PATTERN by mentioning necessity of the re.ASCII flag
1 parent 42e1396 commit 3a92170

File tree

6 files changed

+45
-4
lines changed

6 files changed

+45
-4
lines changed

CHANGELOG.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ Changelog
44
*unreleased*
55
~~~~~~~~~~~~
66

7-
No unreleased changes.
7+
* Fix parsing of ``Version`` and ``Specifier``, to prevent certain
8+
non-ASCII letters from being accepted as a part of the local version
9+
segment (:issue:`469`); also, fix the docs of ``VERSION_PATTERN``, to
10+
mention necessity of the ``re.ASCII`` flag
811

912
21.0 - 2021-07-03
1013
~~~~~~~~~~~~~~~~~

docs/version.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ Reference
284284
The pattern is not anchored at either end, and is intended for embedding
285285
in larger expressions (for example, matching a version number as part of
286286
a file name). The regular expression should be compiled with the
287-
``re.VERBOSE`` and ``re.IGNORECASE`` flags set.
287+
``re.VERBOSE``, ``re.IGNORECASE`` and ``re.ASCII`` flags set.
288288

289289

290290
.. _PEP 440: https://www.python.org/dev/peps/pep-0440/

packaging/specifiers.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,20 @@ class Specifier(_IndividualSpecifier):
411411

412412
_regex = re.compile(r"^\s*" + _regex_str + r"\s*$", re.VERBOSE | re.IGNORECASE)
413413

414+
# Note: an additional check, based of the following regular
415+
# expression, is necessary because without it the 'a-z'
416+
# character ranges in the above regular expression, in
417+
# conjunction with re.IGNORECASE, would cause erroneous
418+
# acceptance of non-ASCII letters in the local version segment
419+
# (see: https://docs.python.org/library/re.html#re.IGNORECASE).
420+
_supplementary_restriction_regex = re.compile(r"""
421+
\s*===.* # No restriction in the identity operator case.
422+
|
423+
[\s\0-\177]* # In all other cases only whitespace characters
424+
# and ASCII-only non-whitespace characters are
425+
# allowed.
426+
""", re.VERBOSE)
427+
414428
_operators = {
415429
"~=": "compatible",
416430
"==": "equal",
@@ -422,6 +436,13 @@ class Specifier(_IndividualSpecifier):
422436
"===": "arbitrary",
423437
}
424438

439+
def __init__(self, spec: str = "", prereleases: Optional[bool] = None) -> None:
440+
super().__init__(spec, prereleases)
441+
442+
match = self._supplementary_restriction_regex.fullmatch(spec)
443+
if not match:
444+
raise InvalidSpecifier(f"Invalid specifier: '{spec}'")
445+
425446
@_require_version_compare
426447
def _compare_compatible(self, prospective: ParsedVersion, spec: str) -> bool:
427448

packaging/version.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,12 +256,21 @@ def _legacy_cmpkey(version: str) -> LegacyCmpKey:
256256

257257
class Version(_BaseVersion):
258258

259-
_regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE)
259+
_regex = re.compile(
260+
VERSION_PATTERN,
261+
262+
# Note: the re.ASCII flag is necessary because without it the
263+
# 'a-z' character ranges in VERSION_PATTERN, in conjunction
264+
# with re.IGNORECASE, would cause erroneous acceptance of
265+
# non-ASCII letters in the local version segment (see:
266+
# https://docs.python.org/library/re.html#re.IGNORECASE).
267+
re.VERBOSE | re.IGNORECASE | re.ASCII,
268+
)
260269

261270
def __init__(self, version: str) -> None:
262271

263272
# Validate the version and parse it into pieces
264-
match = self._regex.search(version)
273+
match = self._regex.fullmatch(version.strip())
265274
if not match:
266275
raise InvalidVersion(f"Invalid version: '{version}'")
267276

tests/test_specifiers.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ def test_specifiers_valid(self, specifier):
8181
# Cannot use a prefix matching after a .devN version
8282
"==1.0.dev1.*",
8383
"!=1.0.dev1.*",
84+
# Local version which includes a non-ASCII letter that
85+
# matches regex '[a-z]' when re.IGNORECASE is in force in
86+
# conjunction with implicit re.UNICODE (i.e., without re.ASCII)
87+
"==1.0+\u0130",
8488
],
8589
)
8690
def test_specifiers_invalid(self, specifier):

tests/test_version.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ def test_valid_versions(self, version):
9696
"1.0+_foobar",
9797
"1.0+foo&asd",
9898
"1.0+1+1",
99+
# Local version which includes a non-ASCII letter that
100+
# matches regex '[a-z]' when re.IGNORECASE is in force in
101+
# conjunction with implicit re.UNICODE (i.e., without re.ASCII)
102+
"1.0+\u0130",
99103
],
100104
)
101105
def test_invalid_versions(self, version):

0 commit comments

Comments
 (0)