github.com/google/osv-scalibr@v0.4.1/semantic/version-debian.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  	"math/big"
    19  	"strings"
    20  )
    21  
    22  func splitAround(s string, sep string, reverse bool) (string, string) {
    23  	var i int
    24  
    25  	if reverse {
    26  		i = strings.LastIndex(s, sep)
    27  	} else {
    28  		i = strings.Index(s, sep)
    29  	}
    30  
    31  	if i == -1 {
    32  		return s, ""
    33  	}
    34  
    35  	return s[:i], s[i+1:]
    36  }
    37  
    38  func splitDebianDigitPrefix(str string) (*big.Int, string, error) {
    39  	// find the index of the first non-digit in the string, which is the end of the prefix
    40  	i := strings.IndexFunc(str, func(c rune) bool {
    41  		return c < 48 || c > 57
    42  	})
    43  
    44  	if i == 0 || str == "" {
    45  		return big.NewInt(0), str, nil
    46  	}
    47  
    48  	if i == -1 {
    49  		i = len(str)
    50  	}
    51  
    52  	digit, err := convertToBigInt(str[:i])
    53  
    54  	if err != nil {
    55  		return nil, "", err
    56  	}
    57  
    58  	return digit, str[i:], nil
    59  }
    60  
    61  func splitDebianNonDigitPrefix(str string) (string, string) {
    62  	// find the index of the first digit in the string, which is the end of the prefix
    63  	i := strings.IndexAny(str, "0123456789")
    64  
    65  	if i == 0 || str == "" {
    66  		return "", str
    67  	}
    68  
    69  	if i == -1 {
    70  		i = len(str)
    71  	}
    72  
    73  	return str[:i], str[i:]
    74  }
    75  
    76  func weighDebianChar(char string) int {
    77  	// tilde and empty take precedent
    78  	if char == "~" {
    79  		return 1
    80  	}
    81  	if char == "" {
    82  		return 2
    83  	}
    84  
    85  	c := int(char[0])
    86  
    87  	// all the letters sort earlier than all the non-letters
    88  	if c < 65 || (c > 90 && c < 97) || c > 122 {
    89  		c += 122
    90  	}
    91  
    92  	return c
    93  }
    94  
    95  func compareDebianVersions(a, b string) (int, error) {
    96  	var ap, bp string
    97  	var adp, bdp *big.Int
    98  	var err error
    99  
   100  	// based off: https://man7.org/linux/man-pages/man7/deb-version.7.html
   101  	for a != "" || b != "" {
   102  		ap, a = splitDebianNonDigitPrefix(a)
   103  		bp, b = splitDebianNonDigitPrefix(b)
   104  
   105  		// First the initial part of each string consisting entirely of
   106  		// non-digit characters is determined...
   107  		if ap != bp {
   108  			apSplit := strings.Split(ap, "")
   109  			bpSplit := strings.Split(bp, "")
   110  
   111  			for i := range max(len(ap), len(bp)) {
   112  				aw := weighDebianChar(fetch(apSplit, i, ""))
   113  				bw := weighDebianChar(fetch(bpSplit, i, ""))
   114  
   115  				if aw < bw {
   116  					return -1, nil
   117  				}
   118  				if aw > bw {
   119  					return +1, nil
   120  				}
   121  			}
   122  		}
   123  
   124  		// Then the initial part of the remainder of each string which
   125  		// consists entirely of digit characters is determined....
   126  		adp, a, err = splitDebianDigitPrefix(a)
   127  
   128  		if err != nil {
   129  			return 0, err
   130  		}
   131  
   132  		bdp, b, err = splitDebianDigitPrefix(b)
   133  
   134  		if err != nil {
   135  			return 0, err
   136  		}
   137  
   138  		if diff := adp.Cmp(bdp); diff != 0 {
   139  			return diff, nil
   140  		}
   141  	}
   142  
   143  	return 0, nil
   144  }
   145  
   146  type debianVersion struct {
   147  	epoch    *big.Int
   148  	upstream string
   149  	revision string
   150  }
   151  
   152  func (v debianVersion) compare(w debianVersion) (int, error) {
   153  	if diff := v.epoch.Cmp(w.epoch); diff != 0 {
   154  		return diff, nil
   155  	}
   156  	if diff, err := compareDebianVersions(v.upstream, w.upstream); diff != 0 || err != nil {
   157  		if err != nil {
   158  			return 0, err
   159  		}
   160  
   161  		return diff, nil
   162  	}
   163  	if diff, err := compareDebianVersions(v.revision, w.revision); diff != 0 || err != nil {
   164  		if err != nil {
   165  			return 0, err
   166  		}
   167  
   168  		return diff, nil
   169  	}
   170  
   171  	return 0, nil
   172  }
   173  
   174  func (v debianVersion) CompareStr(str string) (int, error) {
   175  	w, err := parseDebianVersion(str)
   176  
   177  	if err != nil {
   178  		return 0, err
   179  	}
   180  
   181  	return v.compare(w)
   182  }
   183  
   184  func parseDebianVersion(str string) (debianVersion, error) {
   185  	var upstream, revision string
   186  
   187  	str = strings.TrimSpace(str)
   188  	epoch := big.NewInt(0)
   189  
   190  	if strings.Contains(str, ":") {
   191  		var e string
   192  		var err error
   193  		e, str = splitAround(str, ":", false)
   194  
   195  		if epoch, err = convertToBigInt(e); err != nil {
   196  			return debianVersion{}, err
   197  		}
   198  	}
   199  
   200  	if strings.Contains(str, "-") {
   201  		upstream, revision = splitAround(str, "-", true)
   202  	} else {
   203  		upstream = str
   204  		revision = "0"
   205  	}
   206  
   207  	return debianVersion{epoch, upstream, revision}, nil
   208  }