55# Visit https://aboutcode.org and https://github.com/nexB/univers for support and download.
66
77import attr
8+ import semantic_version
89from packaging .specifiers import SpecifierSet
9- from semantic_version import NpmSpec
1010from semantic_version .base import AllOf
1111from semantic_version .base import AnyOf
1212
@@ -178,7 +178,7 @@ def from_native(cls, string):
178178
179179 # an NpmSpec handles parsing of both the semver versions and node-semver
180180 # ranges at once
181- spec = NpmSpec (string )
181+ spec = semantic_version . NpmSpec (string )
182182
183183 clause = spec .clause .simplify ()
184184 assert isinstance (clause , (AnyOf , AllOf ))
@@ -239,7 +239,7 @@ def from_native(cls, string):
239239
240240 # replace Rubygem ~> pessimistic operator by node-semver equivalent
241241 string = string .replace ("~>" , "~" )
242- spec = NpmSpec (string )
242+ spec = semantic_version . NpmSpec (string )
243243
244244 clause = spec .clause .simplify ()
245245 assert isinstance (clause , (AnyOf , AllOf ))
@@ -266,13 +266,15 @@ class PypiVersionRange(VersionRange):
266266 version_class = versions .PypiVersion
267267
268268 vers_by_native_comparators = {
269+ # 01.01.01 is equal 1.1.1 e.g., with version normalization
269270 "==" : "=" ,
270271 "!=" : "!=" ,
271272 "<=" : "<=" ,
272273 ">=" : ">=" ,
273274 "<" : "<" ,
274275 ">" : ">" ,
275276 "~=" : None ,
277+ # 01.01.01 is NOT equal to 1.1.1 using === which is strict string equality
276278 "===" : None ,
277279 }
278280
@@ -371,8 +373,157 @@ class ArchLinuxVersionRange(VersionRange):
371373
372374
373375class NginxVersionRange (VersionRange ):
376+ """
377+ Nginx versioning is semver for version and their own syntax for ranges as
378+ used in their security advisories.
379+
380+ The documentation on these ranges is minimal. See these for details:
381+ - https://mailman.nginx.org/pipermail/nginx/2021-September/061039.html
382+ - https://nginx.org/en/security_advisories.html
383+ - https://serverfault.com/questions/715049/what-s-the-difference-between-the-mainline-and-stable-branches-of-nginx
384+
385+ In particular for versions:
386+ - the versions are semver.
387+
388+ - versions can be in the one "mainline" branch or one of many "stable" branches.
389+
390+ - for versions in the "mainline" branch, (e.g., development) the minor
391+ segment is an odd number.
392+
393+ - versions in the "stable" branch, (e.g., a release branch) the minor
394+ segment is an even number. Installation are typically made from branch and
395+ its versions.
396+
397+ For example: in 0.6.18 the 6 e.g., semver "minor" segment is either odd or even
398+ - odd (as with "7") means this is the "mainline" branch
399+ - even (as with "4") means this is in a "stable" branch
400+
401+ And for ranges, we have these notations:
402+
403+ - dash ranges: 0.6.18-1.20.0 where start and end are included in the range
404+
405+ - comma ranges: 1.21.0+, 1.20.1+ where any of the condition applies
406+
407+ - plus suffixes: 1.21.0+ where this or any later version in the branch applies
408+ Therefore:
409+ - 1.21.0+ would expand to >=1.21.0 because 21 is odd and this is the
410+ mainline branch
411+
412+ - 1.22.0+ would expand to >=1.22.0,<1.23.0 because 22 is even and this is
413+ one of the stable branches
414+
415+ There are two special version range values:
416+ - "all" means all versions.
417+ - "none" means no version and therefore no version range. It is used only
418+ in one advisory for CVE-2009-4487 and triggers an error.
419+
420+ Some vulnerable ranges are only for Windows builds but the range syntax is
421+ the same. This could be resolved with a specific purl qualifier.
422+ These are prefixed by the string "nginx/Window".
423+ """
424+
374425 scheme = "nginx"
375- version_class = None
426+ version_class = versions .SemverVersion
427+
428+ vers_by_native_comparators = {
429+ "==" : "=" ,
430+ "<=" : "<=" ,
431+ ">=" : ">=" ,
432+ "<" : "<" ,
433+ ">" : ">" ,
434+ }
435+
436+ @classmethod
437+ def from_native (cls , string ):
438+ """
439+ Return a VersionRange built from an nginx range ``string``.
440+
441+ For example:
442+ >>> result = NginxVersionRange.from_native("1.5.10")
443+ >>> assert str(result) == "vers:nginx/1.5.10", str(result)
444+
445+ >>> result = NginxVersionRange.from_native("0.7.52-0.8.39")
446+ >>> assert str(result) == "vers:nginx/<=0.8.39,>=0.7.52", str(result)
447+
448+ >>> result = NginxVersionRange.from_native("1.1.4-1.2.8, 1.3.9-1.4.0")
449+ >>> assert str(result) == "vers:nginx/<=1.2.8,>=1.1.4|<=1.4.0,>=1.3.9", str(result)
450+
451+ >>> result = NginxVersionRange.from_native("0.8.40+, 0.7.66+")
452+ >>> assert str(result) == "vers:nginx/<0.9.0,>=0.8.40|>=0.7.66", str(result)
453+
454+ >>> result = NginxVersionRange.from_native("1.5.0+, 1.4.1+")
455+ >>> assert str(result) == "vers:nginx/<1.5.0,>=1.4.1|>=1.5.0", str(result)
456+
457+ >>> result = NginxVersionRange.from_native("all")
458+ >>> assert str(result) == "vers:nginx/*", str(result)
459+
460+ >>> try:
461+ ... NginxVersionRange.from_native("none")
462+ ... except ValueError:
463+ ... pass
464+ """
465+ cleaned = remove_spaces (string ).lower ()
466+ if cleaned == "all" :
467+ return cls (constraints = [[VersionConstraint (comparator = "*" )]])
468+
469+ anyof_constraints = []
470+
471+ for allof_clauses in cleaned .split ("," ):
472+
473+ if "-" in allof_clauses :
474+ # dash range
475+ start , _ , end = allof_clauses .partition ("-" )
476+ start_version = semantic_version .Version .coerce (start )
477+ end_version = semantic_version .Version .coerce (end )
478+ vstart = VersionConstraint (comparator = ">=" , version = start_version )
479+ vend = VersionConstraint (comparator = "<=" , version = end_version )
480+ allof_constaints = [vstart , vend ]
481+ anyof_constraints .append (allof_constaints )
482+
483+ elif "+" in allof_clauses :
484+ # suffixed version
485+ vs = allof_clauses .rstrip ("+" )
486+ version = semantic_version .Version .coerce (vs )
487+ is_stable = is_even (version .minor )
488+
489+ if is_stable :
490+ # we have a start and end in stable ranges
491+ start_version = semantic_version .Version .coerce (vs )
492+ end_version = start_version .next_minor ()
493+ vstart = VersionConstraint (comparator = ">=" , version = start_version )
494+ vend = VersionConstraint (comparator = "<" , version = end_version )
495+ allof_constaints = [vstart , vend ]
496+ anyof_constraints .append (allof_constaints )
497+ else :
498+ # mainline branch ranges are resolved to a singel constraint
499+ version = semantic_version .Version .coerce (vs )
500+ constraint = VersionConstraint (comparator = ">=" , version = version )
501+ allof_constaints = [constraint ]
502+ anyof_constraints .append (allof_constaints )
503+
504+ else :
505+ # plain single version
506+ version = semantic_version .Version .coerce (allof_clauses )
507+ constraint = VersionConstraint (comparator = "=" , version = version )
508+ allof_constaints = [constraint ]
509+ anyof_constraints .append (allof_constaints )
510+
511+ return cls (constraints = anyof_constraints )
512+
513+
514+ def is_even (s ):
515+ """
516+ Return True if the string "s" is an even number and False if this is an odd
517+ number. For example:
518+
519+ >>> is_even(4)
520+ True
521+ >>> is_even(123)
522+ False
523+ >>> is_even(0)
524+ True
525+ """
526+ return (int (s ) % 2 ) == 0
376527
377528
378529RANGE_CLASS_BY_SCHEMES = {
0 commit comments