Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
218c61a
Add support for NormalizedVersionRanges
keshav-space Mar 14, 2023
1a69d07
Test NormalizedVersionRanges
keshav-space Mar 15, 2023
3b3570c
Add ABOUT and LICENSE file for spans.py
keshav-space Mar 15, 2023
d88b80c
Fallback to builtin set when intbitset is not installed
keshav-space Apr 3, 2023
5ab9b3a
Added docs server script, dark mode & copybutton for docs
OmkarPh Oct 18, 2023
af7e542
Merge pull request #83 from OmkarPh/enhance/docs
AyanSinhaMahapatra Oct 18, 2023
0a9d983
Update CSS to widen page and handle mobile #84
johnmhoran Nov 21, 2023
4e36fc6
Delete theme_overrides_SUPERSEDED.css as no longer needed #84
johnmhoran Jan 16, 2024
7d74b8a
Fix top padding for rst content
AyanSinhaMahapatra Jan 18, 2024
0071028
Merge pull request #85 from nexB/84-widen-rtd-page
AyanSinhaMahapatra Jan 18, 2024
008d521
Update CI runners and python version
AyanSinhaMahapatra Feb 19, 2024
acf94b3
Merge pull request #87 from nexB/update-macos-runners
AyanSinhaMahapatra Feb 19, 2024
124da3d
Replace deprecated macos CI runners
AyanSinhaMahapatra Jul 1, 2024
be4e14d
Update minimum required python version to 3.8
AyanSinhaMahapatra Jul 1, 2024
5c3e935
Merge pull request #90 from nexB/update-ci-runners
keshav-space Jul 1, 2024
f0bac8c
Support both PURL and GitLab schema in from_gitlab_native
keshav-space Jul 19, 2024
629d03a
Use native impl for Maven and NuGet in from_gitlab_native
keshav-space Jul 19, 2024
1c70ea5
Use proper splitter for composer in from_gitlab_native
keshav-space Jul 19, 2024
d109a1b
Support splitting bracket notation ranges
keshav-space Jul 19, 2024
169a6a1
Add support for version range from snyk advisory
keshav-space Jul 22, 2024
e7d7c55
Add support for version range from discrete versions
keshav-space Jul 22, 2024
f8a6d70
Refactor NormalizedVersionRange
keshav-space Jul 23, 2024
d2904b0
Add multi vers test for normalized version range
keshav-space Jul 24, 2024
5a32eda
Fix the edge case resulting in incorrect `contains` resolution
keshav-space Jul 24, 2024
c833b97
Refactor VersionRange normalization without Span
keshav-space Jul 24, 2024
8f0d727
Add function to parse bracket notation constraints
keshav-space Jul 24, 2024
3d0de11
Use from_versions for getting vers from discrete versions
keshav-space Jul 24, 2024
fa7009a
Merge remote-tracking branch 'origin/main' into range_normalization
keshav-space Jul 24, 2024
fe35a34
Merge remote-tracking branch 'skeleton/main' into range_normalization
keshav-space Jul 24, 2024
594baf5
Set `shell` param to False while running code style tests
keshav-space Jul 24, 2024
b12572d
Use only macOS-14 image for macOS 14 CI
keshav-space Jul 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
329 changes: 329 additions & 0 deletions src/univers/normalized_range.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
#
# Copyright (c) nexB Inc. and others.
# SPDX-License-Identifier: Apache-2.0
#
# Visit https://aboutcode.org and https://github.com/nexB/univers for support and download.


import operator
import re
from typing import List
from typing import Union

import attr

from univers.span import Span
from univers.version_range import VersionRange
from univers.versions import AlpineLinuxVersion
from univers.versions import ArchLinuxVersion
from univers.versions import ComposerVersion
from univers.versions import DebianVersion
from univers.versions import GenericVersion
from univers.versions import GentooVersion
from univers.versions import GolangVersion
from univers.versions import MavenVersion
from univers.versions import NginxVersion
from univers.versions import NugetVersion
from univers.versions import OpensslVersion
from univers.versions import PypiVersion
from univers.versions import RpmVersion
from univers.versions import RubygemsVersion
from univers.versions import SemverVersion


Comment thread
keshav-space marked this conversation as resolved.
Outdated
@attr.s(frozen=True, order=False, eq=True, hash=True)
class NormalizedVersionRanges:
"""
A NormalizedVersionRange represents a list of VersionRange resolved
Comment thread
keshav-space marked this conversation as resolved.
Outdated
from diffrent datsource.
Comment thread
keshav-space marked this conversation as resolved.
Outdated
"""

# A tuple of VersionRange
version_ranges = attr.ib(type=tuple, default=attr.Factory(tuple))

def __str__(self):
return "(" + ", ".join([f"'{str(vers)}'" for vers in self.version_ranges]) + ")"

@staticmethod
def get_span_boundry(comparator: str, version: str, version_map: dict):
Comment thread
keshav-space marked this conversation as resolved.
Outdated
"""
Return Span with Lower and Upper boundry limit.
Comment thread
keshav-space marked this conversation as resolved.
Outdated
"""
index = NormalizedVersionRanges.get_version_rank(version, version_map)
resolved_operator = OPERATOR_BY_COMPRATOR.get(comparator, operator.eq)

if resolved_operator == operator.lt:
return Span(1, index - 1)
if resolved_operator == operator.gt:
return Span(index + 1, len(version_map))
Comment thread
keshav-space marked this conversation as resolved.
Outdated
if resolved_operator == operator.ge:
return Span(index, len(version_map))
if resolved_operator == operator.le:
return Span(1, index)
if resolved_operator == operator.eq:
return Span(index)
if resolved_operator == operator.ne:
return Span(1, index - 1).union(Span(index + 1, len(version_map)))

@staticmethod
def get_version_range_from_span(total_span: Span, purl_type: str, reverse_version_map: dict):
"""
Return list containg VersionRange for all subspans in a Span.
Comment thread
keshav-space marked this conversation as resolved.
Outdated
"""
version_ranges = []
list_of_span = total_span.subspans()
for span in list_of_span:
lower_bound = reverse_version_map[span.start]
upper_bound = reverse_version_map[span.end]
vers_exp = (
Comment thread
keshav-space marked this conversation as resolved.
Outdated
f"vers:{purl_type}/{lower_bound}"
if lower_bound == upper_bound
else f"vers:{purl_type}/>={lower_bound}|<={upper_bound}"
)
version_ranges.append(VersionRange.from_string(vers_exp))
return version_ranges

@staticmethod
def get_version_rank(version: str, version_map: dict):
"""
Return equivalent integer ranking for a version.
"""
try:
return version_map[strip_leading_v(version)]
except KeyError as err:
err.args = (f"{version} doesn't exist.",)
raise

@staticmethod
def parse_constraint(constraint: str):
Comment thread
keshav-space marked this conversation as resolved.
Outdated
"""
Return operator and version from a constraint
For example:
>>> assert NormalizedVersionRanges.parse_constraint(">=7.0.0") == ('>=', '7.0.0')
>>> assert NormalizedVersionRanges.parse_constraint("=7.0.0") == ('=', '7.0.0')
>>> assert NormalizedVersionRanges.parse_constraint("[3.0.0") == ('[', '3.0.0')
>>> assert NormalizedVersionRanges.parse_constraint("3.1.25]") == (']', '3.1.25')
"""
if constraint.startswith(("<=", ">=", "==", "!=")):
return constraint[:2], constraint[2:]

if constraint.startswith(("<", ">", "=", "[", "(")):
return constraint[0], constraint[1:]

if constraint.endswith(("]", ")")):
return constraint[-1], constraint[:-1]
return None, constraint

@staticmethod
def get_version_map(versions: List, purl_type: str):
"""
Return dict mapping version to integer.
"""
if purl_type not in VERSIONS_BY_PACKAGE_TYPE:
return
Comment thread
keshav-space marked this conversation as resolved.
Outdated

version_type = VERSIONS_BY_PACKAGE_TYPE.get(purl_type)
sorted_versions = sorted([version_type(i) for i in versions])
Comment thread
keshav-space marked this conversation as resolved.
Outdated
sorted_versions = [version.string for version in sorted_versions]
index = list(range(1, len(sorted_versions) + 1, 1))
return dict(zip(sorted_versions, index))
Comment thread
keshav-space marked this conversation as resolved.
Outdated

@classmethod
def from_github(cls, range_expression: Union[str, List], purl_type: str, all_versions: List):
Comment thread
keshav-space marked this conversation as resolved.
Outdated
"""
Return NormalizedVersionRanges computed from GithHub version range expression.
GitHub range_expression example::
">= 10.4.0, < 10.4.1"
"> 7.1.1"
"""
version_map = cls.get_version_map(all_versions, purl_type)
reverse_version_map = {value: key for key, value in version_map.items()}

items = [range_expression] if isinstance(range_expression, str) else range_expression
total_span = None
for item in items:
gh_constraints = item.strip().replace(" ", "")
constraints = gh_constraints.split(",")
local_span = None
for constraint in constraints:
if not constraint:
continue
gh_comparator, gh_version = cls.parse_constraint(constraint)
span = cls.get_span_boundry(gh_comparator, strip_leading_v(gh_version), version_map)
local_span = span if not local_span else local_span.intersection(span)

total_span = local_span if not total_span else total_span.union(local_span)

version_ranges = cls.get_version_range_from_span(total_span, purl_type, reverse_version_map)
return cls(version_ranges=version_ranges)

@classmethod
def from_snyk(cls, range_expression: Union[str, List], purl_type: str, all_versions: List):
"""
Return NormalizedVersionRanges computed from Snyk version range expression.
Snyk range_expression example::
">=4.0.0, <4.0.10.16"
" >=4.1.0, <4.4.15.7"
"[3.0.0,3.1.25)
"""
version_map = cls.get_version_map(all_versions, purl_type)
reverse_version_map = {value: key for key, value in version_map.items()}

items = [range_expression] if isinstance(range_expression, str) else range_expression
total_span = None
for item in items:
delimiter = "," if "," in item else " "
if delimiter == ",":
snyk_constraints = item.strip().replace(" ", "")
constraints = snyk_constraints.split(",")
else:
snyk_constraints = item.strip()
constraints = snyk_constraints.split(" ")
local_span = None
for constraint in constraints:
if not constraint:
continue
snyk_comparator, snyk_version = cls.parse_constraint(constraint)
if not snyk_version:
continue
span = cls.get_span_boundry(
snyk_comparator, strip_leading_v(snyk_version), version_map
)
local_span = span if not local_span else local_span.intersection(span)

total_span = local_span if not total_span else total_span.union(local_span)

version_ranges = cls.get_version_range_from_span(total_span, purl_type, reverse_version_map)
return cls(version_ranges=version_ranges)

@classmethod
def from_gitlab(cls, range_expression: Union[str, List], purl_type: str, all_versions: List):
"""
Return NormalizedVersionRanges computed from GitLab version range expression.
GitLab range_expression example::
"[7.0.0,7.0.11),[7.2.0,7.2.4)"
"[7.0.0,7.0.11),[7.2.0,7.2.4)"
">=4.0,<4.3||>=5.0,<5.2"
">=0.19.0 <0.30.0"
">=1.5,<1.5.2"
"""

version_map = cls.get_version_map(all_versions, purl_type)
reverse_version_map = {value: key for key, value in version_map.items()}

items = [range_expression] if isinstance(range_expression, str) else range_expression
global_span = None
for item in items:
gitlab_constraints = item.strip()
if gitlab_constraints.startswith(("[", "(")):
# transform "[7.0.0,7.0.11),[7.2.0,7.2.4)" -> [ "[7.0.0,7.0.11)", "[7.2.0,7.2.4)" ]
splitted = gitlab_constraints.split(",")
constraints = [f"{a},{b}" for a, b in zip(splitted[::2], splitted[1::2])]
delimiter = ","

else:
# transform ">=4.0,<4.3||>=5.0,<5.2" -> [ ">=4.0,<4.3", ">=5.0,<5.2" ]
# transform ">=0.19.0 <0.30.0" -> [ ">=0.19.0 <0.30.0" ]
# transform ">=1.5,<1.5.2" -> [ ">=1.5,<1.5.2" ]
delimiter = "," if "," in gitlab_constraints else " "
constraints = gitlab_constraints.split("||")
total_span = None
for constraint in constraints:
local_span = None
for subcontraint in constraint.strip().split(delimiter):
if not subcontraint:
continue
gitlab_comparator, gitlab_version = cls.parse_constraint(subcontraint.strip())
if not gitlab_version:
continue
span = cls.get_span_boundry(
gitlab_comparator, strip_leading_v(gitlab_version), version_map
)
local_span = span if not local_span else local_span.intersection(span)

total_span = local_span if not total_span else total_span.union(local_span)
global_span = total_span if not global_span else global_span.union(total_span)

version_ranges = cls.get_version_range_from_span(
global_span, purl_type, reverse_version_map
)
return cls(version_ranges=version_ranges)

@classmethod
def from_discrete(cls, range_expression: Union[str, List], purl_type: str, all_versions: List):
"""
Return NormalizedVersionRanges computed from discrete version range expression.
Discrete range_expression example::
["1.5","3.1.2","3.1-beta"]
"""
version_map = cls.get_version_map(all_versions, purl_type)
reverse_version_map = {value: key for key, value in version_map.items()}

item = range_expression if isinstance(range_expression, str) else " ".join(range_expression)
discrete_versions = re.split("[ ,\n]+", item)

rank_list = []
for version in discrete_versions:
try:
rank_int = version_map[strip_leading_v(version)]
rank_list.append(rank_int)
except KeyError:
pass

total_span = Span(rank_list)

version_ranges = cls.get_version_range_from_span(total_span, purl_type, reverse_version_map)
return cls(version_ranges)


def strip_leading_v(version: str):
"""
Return version without leading v.
"""
if not version.startswith("v"):
return version
return version[1:]


VERSIONS_BY_PACKAGE_TYPE = {
Comment thread
keshav-space marked this conversation as resolved.
Outdated
"alpine": AlpineLinuxVersion,
"alpm": ArchLinuxVersion,
"apache": SemverVersion,
"cargo": SemverVersion,
# "cocoapods": None,
"composer": ComposerVersion,
# "conan": None,
# "conda": None,
# "cran": None,
"deb": DebianVersion,
"ebuild": GentooVersion,
"gem": RubygemsVersion,
"generic": GenericVersion,
"github": SemverVersion,
"golang": GolangVersion,
"hex": SemverVersion,
"mattermost": SemverVersion,
"maven": MavenVersion,
"mozilla": SemverVersion,
"nginx": NginxVersion,
"npm": SemverVersion,
"nuget": NugetVersion,
"openssl": OpensslVersion,
"pypi": PypiVersion,
"rpm": RpmVersion,
# "swift": None,
}

OPERATOR_BY_COMPRATOR = {
"<": operator.lt,
">": operator.gt,
"=": operator.eq,
"<=": operator.le,
">=": operator.ge,
"==": operator.eq,
"!=": operator.ne,
")": operator.lt,
"]": operator.le,
"(": operator.gt,
"[": operator.ge,
}
Loading