Skip to content

Commit 89ee170

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 * Complement relevant tests, including adding cases related to presence of non-ASCII whitespace characters in input strings * Fix docs of packaging.version.VERSION_PATTERN by mentioning necessity of the re.ASCII flag
1 parent 42e1396 commit 89ee170

File tree

6 files changed

+48
-4
lines changed

6 files changed

+48
-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: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,23 @@ 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(
421+
r"""
422+
\s*===.* # No restriction in the identity operator case.
423+
|
424+
[\s\0-\177]* # In all other cases only whitespace characters
425+
# and ASCII-only non-whitespace characters are
426+
# allowed.
427+
""",
428+
re.VERBOSE,
429+
)
430+
414431
_operators = {
415432
"~=": "compatible",
416433
"==": "equal",
@@ -422,6 +439,13 @@ class Specifier(_IndividualSpecifier):
422439
"===": "arbitrary",
423440
}
424441

442+
def __init__(self, spec: str = "", prereleases: Optional[bool] = None) -> None:
443+
super().__init__(spec, prereleases)
444+
445+
match = self._supplementary_restriction_regex.fullmatch(spec)
446+
if not match:
447+
raise InvalidSpecifier(f"Invalid specifier: '{spec}'")
448+
425449
@_require_version_compare
426450
def _compare_compatible(self, prospective: ParsedVersion, spec: str) -> bool:
427451

packaging/version.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,12 +256,20 @@ 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+
# Note: the re.ASCII flag is necessary because without it the
262+
# 'a-z' character ranges in VERSION_PATTERN, in conjunction
263+
# with re.IGNORECASE, would cause erroneous acceptance of
264+
# non-ASCII letters in the local version segment (see:
265+
# https://docs.python.org/library/re.html#re.IGNORECASE).
266+
re.VERBOSE | re.IGNORECASE | re.ASCII,
267+
)
260268

261269
def __init__(self, version: str) -> None:
262270

263271
# Validate the version and parse it into pieces
264-
match = self._regex.search(version)
272+
match = self._regex.fullmatch(version.strip())
265273
if not match:
266274
raise InvalidVersion(f"Invalid version: '{version}'")
267275

tests/test_specifiers.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ 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 matches
85+
# regex '[a-z]' when re.IGNORECASE is in force and re.ASCII is not
86+
"==1.0+\u0130",
8487
],
8588
)
8689
def test_specifiers_invalid(self, specifier):
@@ -197,6 +200,7 @@ def test_specifiers_invalid(self, specifier):
197200
# Various other normalizations
198201
"v1.0",
199202
" \r \f \v v1.0\t\n",
203+
" \r\N{NARROW NO-BREAK SPACE}\v v1.0\N{PARAGRAPH SEPARATOR}",
200204
],
201205
)
202206
def test_specifiers_normalized(self, version):
@@ -221,6 +225,7 @@ def test_specifiers_normalized(self, version):
221225
("~=2.0", "~=2.0"),
222226
# Spaces should be removed
223227
("< 2", "<2"),
228+
("<\N{HAIR SPACE}2", "<2"),
224229
],
225230
)
226231
def test_specifiers_str_and_repr(self, specifier, expected):

tests/test_version.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ 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 matches
100+
# regex '[a-z]' when re.IGNORECASE is in force and re.ASCII is not
101+
"1.0+\u0130",
99102
],
100103
)
101104
def test_invalid_versions(self, version):
@@ -218,6 +221,7 @@ def test_invalid_versions(self, version):
218221
# Various other normalizations
219222
("v1.0", "1.0"),
220223
(" v1.0\t\n", "1.0"),
224+
("\N{NARROW NO-BREAK SPACE}1.0\t\N{PARAGRAPH SEPARATOR}\n ", "1.0"),
221225
],
222226
)
223227
def test_normalized_versions(self, version, normalized):

0 commit comments

Comments
 (0)