Skip to content

Commit 8b84788

Browse files
committed
Implement vers spec validations
Signed-off-by: Philippe Ombredanne <pombredanne@nexb.com>
1 parent 021247c commit 8b84788

2 files changed

Lines changed: 54 additions & 9 deletions

File tree

src/univers/version_constraint.py

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66

77
import operator
88
from functools import total_ordering
9-
import attr
109

10+
import attr
1111
from univers.utils import remove_spaces
1212
from univers.versions import Version
1313

@@ -125,8 +125,14 @@ def from_string(cls, string, version_class):
125125
a ``version_class`` Version class.
126126
"""
127127
constraint_string = remove_spaces(string)
128-
comparator, version = cls.split(constraint_string)
129128

129+
# A version range specifier contains only printable ASCII letters, digits and
130+
# punctuation.
131+
is_ascii = len(constraint_string) + 2 == len(ascii(constraint_string))
132+
if not is_ascii:
133+
raise ValueError(f"Invalid non ASCII characters: {string!r}")
134+
135+
comparator, version = cls.split(constraint_string)
130136
if comparator not in COMPARATORS:
131137
raise ValueError(f"Unknown comparator: {comparator!r}")
132138

@@ -230,10 +236,18 @@ def validate(cls, constraints):
230236
if not all(isinstance(c, VersionConstraint) for c in constraints):
231237
raise ValueError(f"{constraints!r} can contain only VersionConstraint")
232238

239+
# Versions are unique. Each ``version`` must be unique in a range and can
240+
# occur only once in any ``<version-constraint>`` of a range specifier,
241+
# irrespective of its comparators. Tools must report an error for duplicated
242+
# versions.
233243
if len(set(c.version for c in constraints)) != len(constraints):
234244
raise ValueError(f"{constraints!r} cannot contain duplicated Version")
235245

246+
# Constraints are sorted by version**. The canonical ordering is the versions
247+
# order. The ordering of ``<version-constraint>`` is not significant otherwise
248+
# but this sort order is needed when check if a version is contained in a range.
236249
constraints.sort()
250+
237251
return validate_comparators(constraints)
238252

239253
@classmethod
@@ -282,17 +296,28 @@ def validate_comparators(constraints):
282296
- ">" and ">=" can only be followed by one of "<", "<="
283297
"""
284298

299+
# Starting from a de-duplicated and sorted list of constraints, these extra rules
300+
# apply to the comparators of any two contiguous constraints to be valid:
301+
302+
# There is only one star: "*" must only occur once and alone in a range,
303+
# without any other constraint or version.
285304
if any(c.comparator == "*" for c in constraints):
286305
if len(constraints) != 1:
287306
raise ValueError(f"Invalid {constraints!r}: can contain only one star '*'")
288307
return True
289308

290-
# discard != that can occur anywhere
309+
# "!=" constraint can be followed by a constraint using any comparator, i.e.,
310+
# any of "=", "!=", ">", ">=", "<", "<=" as comparator (or no constraint).
311+
312+
# Ignoring all constraints with "!=" comparators:
313+
# --> discard != that can occur anywhere
291314
constraints = [c for c in constraints if c.comparator != "!="]
292315
if not constraints:
293316
return True
294317

295-
# check that equals is followed only by "=", ">", ">="
318+
# A "=" constraint must be followed only by a constraint with one of "=", ">",
319+
# ">=" as comparator (or no constraint).
320+
# --> check that equals is followed only by "=", ">", ">="
296321
invalid_equal = [
297322
(cur, nxt)
298323
for cur, nxt in pairwise(constraints)
@@ -305,16 +330,22 @@ def validate_comparators(constraints):
305330
f"Invalid {c!r}: where {i!r} " "cannot contain an equal = followed by either < or <="
306331
)
307332

308-
# discard = that have been validated above
333+
# And ignoring all constraints with "=" or "!=" comparators:
334+
# --> discard = that have been validated above
309335
constraints = [c for c in constraints if c.comparator != "="]
310336
if not constraints:
311337
return True
312338

313-
# from now on this must be an alternation of greater/lesser
339+
# the sequence of constraint comparators must be an alternation of greater
340+
# and lesser comparators:
341+
# --> from now on this must be an alternation of greater/lesser
314342
for cur_constraint, nxt_constraint in pairwise(constraints):
315343
cur_comp = cur_constraint.comparator
316344
nxt_comp = nxt_constraint.comparator
317345

346+
# "<" and "<=" must be followed by one of ">", ">=" (or no constraint).
347+
# ">" and ">=" must be followed by one of "<", "<=" (or no constraint).
348+
# Tools must report an error for such invalid ranges.
318349
if (cur_comp in ("<", "<=") and nxt_comp not in (">", ">=")) or (
319350
cur_comp in (">", ">=") and nxt_comp not in ("<", "<=")
320351
):
@@ -373,11 +404,11 @@ def simplify_constraints(constraints):
373404
# discard current constraint
374405
constraints.pop(i)
375406
# Previous constraint becomes current if if exists.
376-
if i:
407+
if i > 0:
377408
i -= 1
378409

379410
# If there is a previous constraint:
380-
if i:
411+
if i > 0:
381412

382413
prv = constraints[i - 1]
383414
prv_comp = prv.comparator

src/univers/version_range.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,16 @@ def from_string(cls, vers, simplify=False, validate=False):
7676
Return a VersionRange built from a ``vers`` version range spec string,
7777
such as "vers:npm/1.2.3,>=2.0.0"
7878
"""
79+
# Spaces are not significant and removed in a canonical form.
7980
vers = remove_spaces(vers)
8081

82+
# A version range specifier contains only printable ASCII letters, digits and
83+
# punctuation.
84+
is_ascii = len(vers) + 2 == len(ascii(vers))
85+
if not is_ascii:
86+
raise ValueError(f"Invalid non ASCII characters: {vers!r}")
87+
88+
# The URI scheme and versioning scheme are always lowercase as in ``vers:npm``.
8189
uri_scheme, _, scheme_range_spec = vers.partition(":")
8290
uri_scheme = uri_scheme.lower()
8391

@@ -94,10 +102,12 @@ def from_string(cls, vers, simplify=False, validate=False):
94102

95103
version_class = range_class.version_class
96104

97-
constraints = constraints.strip()
105+
constraints = remove_spaces(constraints)
98106
if not constraints:
99107
raise ValueError(f"{vers!r} specifies no version range constraints.")
100108

109+
# There is only one star: "*" must only occur once and alone in a range,
110+
# without any other constraint or version.
101111
if constraints.startswith("*"):
102112
if constraints != "*":
103113
raise ValueError(f"{vers!r} contains an invalid '*' constraint.")
@@ -113,7 +123,11 @@ def from_string(cls, vers, simplify=False, validate=False):
113123
)
114124
parsed_constraints.append(constraint)
115125

126+
# Constraints are sorted by version**. The canonical ordering is the versions
127+
# order. The ordering of ``<version-constraint>`` is not significant otherwise
128+
# but this sort order is needed when check if a version is contained in a range.
116129
parsed_constraints.sort()
130+
117131
if simplify:
118132
parsed_constraints = VersionConstraint.simplify(parsed_constraints)
119133
if validate:

0 commit comments

Comments
 (0)