github.com/google/osv-scalibr@v0.4.1/semantic/version-semver.go (about)

     1  // Copyright 2025 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package semantic
    16  
    17  import (
    18  	"strings"
    19  )
    20  
    21  // Removes build metadata from the given string if present, per semver v2
    22  //
    23  // See https://semver.org/spec/v2.0.0.html#spec-item-10
    24  func removeBuildMetadata(str string) string {
    25  	parts := strings.Split(str, "+")
    26  
    27  	return parts[0]
    28  }
    29  
    30  func compareBuildComponents(a, b string) int {
    31  	// https://semver.org/spec/v2.0.0.html#spec-item-10
    32  	a = removeBuildMetadata(a)
    33  	b = removeBuildMetadata(b)
    34  
    35  	// the spec doesn't explicitly say "don't include the hyphen in the compare"
    36  	// but it's what node-semver does so for now let's go with that...
    37  	a = strings.TrimPrefix(a, "-")
    38  	b = strings.TrimPrefix(b, "-")
    39  
    40  	// versions with a prerelease are considered less than those without
    41  	// https://semver.org/spec/v2.0.0.html#spec-item-9
    42  	if a == "" && b != "" {
    43  		return +1
    44  	}
    45  	if a != "" && b == "" {
    46  		return -1
    47  	}
    48  
    49  	return compareSemverBuildComponents(
    50  		strings.Split(a, "."),
    51  		strings.Split(b, "."),
    52  	)
    53  }
    54  
    55  func compareSemverBuildComponents(a, b []string) int {
    56  	minComponentLength := min(len(a), len(b))
    57  
    58  	var compare int
    59  
    60  	for i := range minComponentLength {
    61  		ai, aErr := convertToBigInt(a[i])
    62  		bi, bErr := convertToBigInt(b[i])
    63  
    64  		switch {
    65  		// 1. Identifiers consisting of only digits are compared numerically.
    66  		case aErr == nil && bErr == nil:
    67  			compare = ai.Cmp(bi)
    68  		// 2. Identifiers with letters or hyphens are compared lexically in ASCII sort order.
    69  		case aErr != nil && bErr != nil:
    70  			compare = strings.Compare(a[i], b[i])
    71  		// 3. Numeric identifiers always have lower precedence than non-numeric identifiers.
    72  		case aErr == nil:
    73  			compare = -1
    74  		default:
    75  			compare = +1
    76  		}
    77  
    78  		if compare != 0 {
    79  			if compare > 0 {
    80  				return 1
    81  			}
    82  
    83  			return -1
    84  		}
    85  	}
    86  
    87  	// 4. A larger set of pre-release fields has a higher precedence than a smaller set,
    88  	//    if all the preceding identifiers are equal.
    89  	if len(a) > len(b) {
    90  		return +1
    91  	}
    92  	if len(a) < len(b) {
    93  		return -1
    94  	}
    95  
    96  	return 0
    97  }
    98  
    99  type semverVersion struct {
   100  	semverLikeVersion
   101  }
   102  
   103  func parseSemverVersion(str string) semverVersion {
   104  	return semverVersion{parseSemverLikeVersion(str, 3)}
   105  }
   106  
   107  func (v semverVersion) compare(w semverVersion) int {
   108  	if diff := v.Components.Cmp(w.Components); diff != 0 {
   109  		return diff
   110  	}
   111  
   112  	return compareBuildComponents(v.Build, w.Build)
   113  }
   114  
   115  func (v semverVersion) CompareStr(str string) (int, error) {
   116  	return v.compare(parseSemverVersion(str)), nil
   117  }