github.com/google/osv-scalibr@v0.4.1/semantic/version-packagist.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  	"regexp"
    19  	"strconv"
    20  	"strings"
    21  )
    22  
    23  var (
    24  	packagistVersionSeperatorFinder = regexp.MustCompile(`[-_+]`)
    25  	packagistNonDigitToDigitFinder  = regexp.MustCompile(`([^\d.])(\d)`)
    26  	packagistDigitToNonDigitFinder  = regexp.MustCompile(`(\d)([^\d.])`)
    27  )
    28  
    29  func canonicalizePackagistVersion(v string) string {
    30  	// todo: decide how to handle this - without it, we're 1:1 with the native
    31  	//   PHP version_compare function, but composer removes it; arguably this
    32  	//   should be done before the version is passed in (by the dev), except
    33  	//   the ecosystem is named "Packagist" not "php version_compare", though
    34  	//   packagist itself doesn't seem to enforce this (its composer that does
    35  	//   the trimming...)
    36  	v = strings.TrimPrefix(strings.TrimPrefix(v, "v"), "V")
    37  
    38  	v = packagistVersionSeperatorFinder.ReplaceAllString(v, ".")
    39  	v = packagistNonDigitToDigitFinder.ReplaceAllString(v, "$1.$2")
    40  	v = packagistDigitToNonDigitFinder.ReplaceAllString(v, "$1.$2")
    41  
    42  	return v
    43  }
    44  
    45  func weighPackagistBuildCharacter(str string) int {
    46  	if strings.HasPrefix(str, "RC") {
    47  		return 3
    48  	}
    49  
    50  	specials := []string{"dev", "a", "b", "rc", "#", "p"}
    51  
    52  	for i, special := range specials {
    53  		if strings.HasPrefix(str, special) {
    54  			return i
    55  		}
    56  	}
    57  
    58  	return 0
    59  }
    60  
    61  func comparePackagistSpecialVersions(a, b string) int {
    62  	av := weighPackagistBuildCharacter(a)
    63  	bv := weighPackagistBuildCharacter(b)
    64  
    65  	if av > bv {
    66  		return 1
    67  	} else if av < bv {
    68  		return -1
    69  	}
    70  
    71  	return 0
    72  }
    73  
    74  func comparePackagistComponents(a, b []string) int {
    75  	minLength := min(len(a), len(b))
    76  
    77  	var compare int
    78  
    79  	for i := range minLength {
    80  		ai, aErr := convertToBigInt(a[i])
    81  		bi, bErr := convertToBigInt(b[i])
    82  
    83  		switch {
    84  		case aErr == nil && bErr == nil:
    85  			compare = ai.Cmp(bi)
    86  		case aErr != nil && bErr != nil:
    87  			compare = comparePackagistSpecialVersions(a[i], b[i])
    88  		case aErr == nil:
    89  			compare = comparePackagistSpecialVersions("#", b[i])
    90  		default:
    91  			compare = comparePackagistSpecialVersions(a[i], "#")
    92  		}
    93  
    94  		if compare != 0 {
    95  			if compare > 0 {
    96  				return 1
    97  			}
    98  
    99  			return -1
   100  		}
   101  	}
   102  
   103  	if len(a) > len(b) {
   104  		next := a[len(b)]
   105  
   106  		if _, err := strconv.Atoi(next); err == nil {
   107  			return 1
   108  		}
   109  
   110  		return comparePackagistComponents(a[len(b):], []string{"#"})
   111  	}
   112  
   113  	if len(a) < len(b) {
   114  		next := b[len(a)]
   115  
   116  		if _, err := strconv.Atoi(next); err == nil {
   117  			return -1
   118  		}
   119  
   120  		return comparePackagistComponents([]string{"#"}, b[len(a):])
   121  	}
   122  
   123  	return 0
   124  }
   125  
   126  type packagistVersion struct {
   127  	Original   string
   128  	Components []string
   129  }
   130  
   131  func parsePackagistVersion(str string) packagistVersion {
   132  	return packagistVersion{
   133  		str,
   134  		strings.Split(canonicalizePackagistVersion(str), "."),
   135  	}
   136  }
   137  
   138  func (v packagistVersion) compare(w packagistVersion) int {
   139  	return comparePackagistComponents(v.Components, w.Components)
   140  }
   141  
   142  func (v packagistVersion) CompareStr(str string) (int, error) {
   143  	return v.compare(parsePackagistVersion(str)), nil
   144  }