From 188383b78dca13dfba769cac685ed7fe462f1002 Mon Sep 17 00:00:00 2001 From: Cristian Ramirez <62491943+Str0k@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:41:43 -0500 Subject: [PATCH] Reject non-ASCII digits and trailing newlines in version parsing Co-authored-by: Codex --- changelog.d/pr478.bugfix.rst | 3 +++ src/semver/version.py | 6 +++--- tests/test_strict_parsing.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 changelog.d/pr478.bugfix.rst create mode 100644 tests/test_strict_parsing.py diff --git a/changelog.d/pr478.bugfix.rst b/changelog.d/pr478.bugfix.rst new file mode 100644 index 00000000..87de2218 --- /dev/null +++ b/changelog.d/pr478.bugfix.rst @@ -0,0 +1,3 @@ +``Version.parse()`` and ``Version.is_valid()`` now reject non-ASCII digits and +trailing newlines, in accordance with the SemVer grammar. Optional minor and +patch parsing applies the same character restrictions. diff --git a/src/semver/version.py b/src/semver/version.py index 6fef00bc..0cfd2131 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -108,17 +108,17 @@ class Version: [0-9a-zA-Z-]+ (?:\.[0-9a-zA-Z-]+)* ))? - $ + \Z """ #: Regex for a semver version _REGEX: ClassVar[Pattern[str]] = re.compile( _REGEX_TEMPLATE.format(opt_patch="", opt_minor=""), - re.VERBOSE, + re.VERBOSE | re.ASCII, ) #: Regex for a semver version that might be shorter _REGEX_OPTIONAL_MINOR_AND_PATCH: ClassVar[Pattern[str]] = re.compile( _REGEX_TEMPLATE.format(opt_patch="?", opt_minor="?"), - re.VERBOSE, + re.VERBOSE | re.ASCII, ) def __init__( diff --git a/tests/test_strict_parsing.py b/tests/test_strict_parsing.py new file mode 100644 index 00000000..b50bf5d1 --- /dev/null +++ b/tests/test_strict_parsing.py @@ -0,0 +1,33 @@ +import pytest + +from semver import Version + + +@pytest.mark.parametrize( + "version", + [ + "1.2.3\n", + "1.2.3-rc.1\n", + "1.2.3+build.1\n", + "1\u0662.2.3", + "1.2\u0663.3", + "1.2.3\u0664", + "1.2.3-1\u0662", + "1.2.3-\u0661alpha", + ], +) +@pytest.mark.parametrize("optional_minor_and_patch", [False, True]) +def test_parse_rejects_non_semver_characters(version, optional_minor_and_patch): + with pytest.raises(ValueError): + Version.parse(version, optional_minor_and_patch=optional_minor_and_patch) + + +@pytest.mark.parametrize("version", ["1\n", "1.2\n", "1\u0662", "1.2\u0663"]) +def test_optional_parse_rejects_non_semver_characters(version): + with pytest.raises(ValueError): + Version.parse(version, optional_minor_and_patch=True) + + +@pytest.mark.parametrize("version", ["1.2.3\n", "1\u0662.2.3", "1.2.3-\u0661alpha"]) +def test_is_valid_rejects_non_semver_characters(version): + assert not Version.is_valid(version)