From 5607d4380aeba6e798f1c3338cdd846ff1d9b41d Mon Sep 17 00:00:00 2001 From: Adrien Cotte Date: Tue, 15 Sep 2026 15:20:10 +0200 Subject: [PATCH] Accept abbreviated upper bounds in version ranges Range matching already includes more precise versions of an abbreviated upper bound, but parsing rejects ranges whose lower bound extends that upper bound. Allow these ranges so callers can constrain a minimum version to the same major or minor version without inventing an upper limit. Fixes #698 Assisted-by: Codex:gpt-6-astra Signed-off-by: Adrien Cotte --- NEWS.rst | 2 ++ .../advanced-module-version-specifiers.rst | 8 ++++++++ doc/source/module.rst | 3 +++ doc/source/modulefile.rst | 3 +++ tcl/modspec.tcl | 4 +++- .../272-adv_version_spec-range.exp | 17 +++++++++++++++++ 6 files changed, 36 insertions(+), 1 deletion(-) diff --git a/NEWS.rst b/NEWS.rst index 5601b8078..a15d75bd5 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -255,6 +255,8 @@ Modules 5.7.0 (not yet released) shell meta-characters. Completion candidates were passed to ``compgen -W`` which evaluates command substitution syntax. (fix `CVE-2026-85013`_ found by AISLE in partnership with Red Hat) +* Accept abbreviated upper bounds in version ranges such as ``@3.20:3`` to + select versions starting at ``3.20`` within major version ``3``. .. _CVE-2026-85013: https://github.com/envmodules/modules/security/advisories/GHSA-8hrw-p88g-qhmg diff --git a/doc/source/design/advanced-module-version-specifiers.rst b/doc/source/design/advanced-module-version-specifiers.rst index 7828fdcc1..7b14b5dd1 100644 --- a/doc/source/design/advanced-module-version-specifiers.rst +++ b/doc/source/design/advanced-module-version-specifiers.rst @@ -91,6 +91,14 @@ Specification - for instance *@1:1.10* will matches *1.0* and *1.8* but not *1.12* + - the upper bound may be less precise than the lower bound + + - an abbreviated upper bound includes all versions that extend it, + provided they also satisfy the lower bound + - for instance *@1.2:1* matches *1.2* and *1.10* but not *1.1* or *2.0* + - a reversed range remains invalid unless the lower bound extends the + abbreviated upper bound, so *@2.0:1* is still rejected + - version could be specified as list - soft@vers,vers,vers diff --git a/doc/source/module.rst b/doc/source/module.rst index b9ab6e5ed..e0f58d3e6 100644 --- a/doc/source/module.rst +++ b/doc/source/module.rst @@ -3235,6 +3235,9 @@ the activation of the extended default mechanism (see to refer to more precise version numbers like ``1.2.3``. Characters ``.`` and ``-`` are considered version number separator to determine abbreviated versions. Range of versions on its side natively handles abbreviated versions. +The upper bound may be less precise than the lower bound. An abbreviated +upper bound includes all versions that extend it, provided they also satisfy +the lower bound. In order to be specified in a range of versions or compared to a range of versions, the version major element should corresponds to a number. For diff --git a/doc/source/modulefile.rst b/doc/source/modulefile.rst index 49194f469..cf34ef69d 100644 --- a/doc/source/modulefile.rst +++ b/doc/source/modulefile.rst @@ -2137,6 +2137,9 @@ notation like ``@1`` to refer to more precise version numbers like ``1.2.3``. Characters ``.`` and ``-`` are considered version number separator to determine abbreviated versions. Range of versions on its side natively handles abbreviated versions. +The upper bound may be less precise than the lower bound. An abbreviated +upper bound includes all versions that extend it, provided they also satisfy +the lower bound. In order to be specified in a range of versions or compared to a range of versions, the version major element should corresponds to a number. For diff --git a/tcl/modspec.tcl b/tcl/modspec.tcl index 97bf7e488..a614a449c 100644 --- a/tcl/modspec.tcl +++ b/tcl/modspec.tcl @@ -1246,7 +1246,9 @@ proc parseModuleVersionSpecifier {modspec} { set cmpspec le set versspec $hivers # between or equal - } elseif {[versioncmp $lovers $hivers] == 1} { + # An abbreviated upper bound includes its more precise versions. + } elseif {[versioncmp $lovers $hivers] == 1 && ![string match\ + $hivers[extendedDefaultCharGlobMatch] $lovers]} { set invalidversrange 1 } else { set cmpspec be diff --git a/testsuite/modules.70-maint/272-adv_version_spec-range.exp b/testsuite/modules.70-maint/272-adv_version_spec-range.exp index 0e4ac79de..6af813afb 100644 --- a/testsuite/modules.70-maint/272-adv_version_spec-range.exp +++ b/testsuite/modules.70-maint/272-adv_version_spec-range.exp @@ -84,6 +84,8 @@ testouterr_cmd sh {load mod@1::} ERR "$err_specvers'1::'" testouterr_cmd sh {load mod@1,:,} ERR "$err_specvers'1,:,'" testouterr_cmd sh {load mod@,:} ERR "$err_specvers',:'" testouterr_cmd sh {load mod@1.3:1.2} ERR "$err_rangevers'1.3:1.2'" +testouterr_cmd sh {load mod@4.1:3} ERR "$err_rangevers'4.1:3'" +testouterr_cmd sh {load mod@1.30:1.3} ERR "$err_rangevers'1.30:1.3'" testouterr_cmd sh {load mod@,<} ERR "$err_specvers',<'" testouterr_cmd sh {load mod@:<} ERR "$err_rangevers':<'" testouterr_cmd sh {load mod@1/1:2} ERR "$err_specvers'1/1:2'" @@ -113,6 +115,7 @@ testouterr_cmd sh {load extdfl @1.2:} $ans {} testouterr_cmd sh {load extdfl @:1.3.1} $ans {} testouterr_cmd sh {load extdfl @:1.3.7} $ans {} testouterr_cmd sh {load extdfl @1.2:1.3} $ans {} +testouterr_cmd sh {load extdfl @1.2:1} $ans {} testouterr_cmd sh {load extdfl @1.2.3:1.3.7} $ans {} testouterr_cmd sh {load extdfl @:2.0.1} $ans {} testouterr_cmd sh {load extdfl @:2.0} $ans {} @@ -162,6 +165,7 @@ testouterr_cmd sh {path extdfl/ @1.2:1.3} [list [list text $mp/extdfl/1.3.1]] {} # test all sub-cmd concerned by context testouterr_cmd sh {avail -t extdfl @1.3:1.4} OK "$mp:\nextdfl/1.3.1(default)\nextdfl/1.3.7\nextdfl/1.4.5" +testouterr_cmd sh {avail -t extdfl @1.3.7:1} OK "$mp:\nextdfl/1.3.7\nextdfl/1.4.5" testouterr_cmd_re sh {whatis extdfl@1.3.7:1.4.5} OK "$modlin $mpre $modlin \\s+extdfl/1.3.7: extdfl/1.3.7 \\s+extdfl/1.4.5: extdfl/1.4.5" @@ -187,6 +191,18 @@ testouterr_cmd sh {spider -t ext?fl @1.3:} OK "$mp:\nextdfl/1.3.1(default)\nextd testouterr_cmd sh {avail -t e??.* @:1.4} OK "$mp:\ne.t.fl/1.3.1\ne.t.fl/1.3.7\ne.t.fl/1.4.5" # additional version tests +# Abbreviated upper bounds apply independently of extended defaults. +foreach extdfl {0 1} { + setenv_var MODULES_EXTENDED_DEFAULT $extdfl + set ans [list [list text $mp/extdfl/1.3.7] [list text $mp/extdfl/1.4.5]] + testouterr_cmd sh {paths extdfl@1.3.7:1} $ans {} + testouterr_cmd sh {paths -i EXTDFL@1.3.7:1} $ans {} + set ans [list [list text $mp/extdfl/1.3.7]] + testouterr_cmd sh {paths extdfl@1.3.7:1.3} $ans {} + testouterr_cmd sh {paths extdfl@1.3.7:1.3,9:10} $ans {} +} +unsetenv_var MODULES_EXTENDED_DEFAULT + testouterr_cmd sh {avail -t extdfl8 @1.33:} OK "$mp:\nextdfl8/1.33" testouterr_cmd sh {avail -t extdfl8 @:1.3} OK "$mp:\nextdfl8/1.3.1\nextdfl8/1.3.7" testouterr_cmd sh {spider -t extdfl8 @:1.3} OK "$mp:\nextdfl8/1.3.1\nextdfl8/1.3.7" @@ -297,6 +313,7 @@ set ans [list] lappend ans [list set _LMFILES_ $mp/extdfl2/3.1.7] lappend ans [list set LOADEDMODULES extdfl2/3.1.7] testouterr_cmd sh {load extdfl2@3.0.2:3.1.7} $ans {} +testouterr_cmd sh {load extdfl2@3.1.7:3.1} $ans {} testouterr_cmd sh {load extdfl2@:3.1.7} $ans {} testouterr_cmd sh {load extdfl2@:3.1.7} $ans {} testouterr_cmd sh "load $mp/extdfl2 @:3.1.7" ERR "$err_file'$mp/extdfl2 @:3.1.7'"