go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/providers/core/resources/versions/rpm/version.go (about)

     1  // Copyright 2017 clair authors
     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 rpm implements a versionfmt.Parser for version numbers used in rpm
    16  // based software packages.
    17  package rpm
    18  
    19  import (
    20  	"errors"
    21  	"math"
    22  	"regexp"
    23  	"strconv"
    24  	"strings"
    25  	"unicode"
    26  )
    27  
    28  const (
    29  	// MinVersion is a special package version which is always sorted first.
    30  	MinVersion = "#MINV#"
    31  
    32  	// MaxVersion is a special package version which is always sorted last.
    33  	MaxVersion = "#MAXV#"
    34  )
    35  
    36  // ParserName is the name by which the rpm parser is registered.
    37  const ParserName = "rpm"
    38  
    39  var (
    40  	// alphanumPattern is a regular expression to match all sequences of numeric
    41  	// characters or alphanumeric characters.
    42  	alphanumPattern = regexp.MustCompile("([a-zA-Z]+)|([0-9]+)|(~)")
    43  	allowedSymbols  = []rune{'.', '-', '+', '~', ':', '_'}
    44  )
    45  
    46  type version struct {
    47  	epoch   int
    48  	version string
    49  	release string
    50  }
    51  
    52  var (
    53  	minVersion = version{version: MinVersion}
    54  	maxVersion = version{version: MaxVersion}
    55  )
    56  
    57  // newVersion parses a string into a version type which can be compared.
    58  func newVersion(str string) (version, error) {
    59  	var v version
    60  
    61  	// Trim leading and trailing space
    62  	str = strings.TrimSpace(str)
    63  
    64  	if len(str) == 0 {
    65  		return version{}, errors.New("Version string is empty")
    66  	}
    67  
    68  	// Max/Min versions
    69  	if str == maxVersion.String() {
    70  		return maxVersion, nil
    71  	}
    72  	if str == minVersion.String() {
    73  		return minVersion, nil
    74  	}
    75  
    76  	// Find epoch
    77  	sepepoch := strings.Index(str, ":")
    78  	if sepepoch > -1 {
    79  		intepoch, err := strconv.Atoi(str[:sepepoch])
    80  		if err == nil {
    81  			v.epoch = intepoch
    82  		} else {
    83  			return version{}, errors.New("epoch in version is not a number")
    84  		}
    85  		if intepoch < 0 {
    86  			return version{}, errors.New("epoch in version is negative")
    87  		}
    88  	} else {
    89  		v.epoch = 0
    90  	}
    91  
    92  	// Find version / release
    93  	seprevision := strings.Index(str, "-")
    94  	if seprevision > -1 {
    95  		v.version = str[sepepoch+1 : seprevision]
    96  		v.release = str[seprevision+1:]
    97  	} else {
    98  		v.version = str[sepepoch+1:]
    99  		v.release = ""
   100  	}
   101  	// Verify format
   102  	if len(v.version) == 0 {
   103  		return version{}, errors.New("No version")
   104  	}
   105  
   106  	for i := 0; i < len(v.version); i = i + 1 {
   107  		r := rune(v.version[i])
   108  		if !unicode.IsDigit(r) && !unicode.IsLetter(r) && !validSymbol(r) {
   109  			return version{}, errors.New("invalid character in version")
   110  		}
   111  	}
   112  
   113  	for i := 0; i < len(v.release); i = i + 1 {
   114  		r := rune(v.release[i])
   115  		if !unicode.IsDigit(r) && !unicode.IsLetter(r) && !validSymbol(r) {
   116  			return version{}, errors.New("invalid character in revision")
   117  		}
   118  	}
   119  
   120  	return v, nil
   121  }
   122  
   123  type Parser struct{}
   124  
   125  func (p Parser) Valid(str string) bool {
   126  	_, err := newVersion(str)
   127  	return err == nil
   128  }
   129  
   130  func (p Parser) InRange(versionA, rangeB string) (bool, error) {
   131  	cmp, err := p.Compare(versionA, rangeB)
   132  	if err != nil {
   133  		return false, err
   134  	}
   135  	return cmp < 0, nil
   136  }
   137  
   138  func (p Parser) GetFixedIn(fixedIn string) (string, error) {
   139  	// In the old version format parser design, the string to determine fixed in
   140  	// version is the fixed in version.
   141  	return fixedIn, nil
   142  }
   143  
   144  func (p Parser) Compare(a, b string) (int, error) {
   145  	v1, err := newVersion(a)
   146  	if err != nil {
   147  		return 0, err
   148  	}
   149  
   150  	v2, err := newVersion(b)
   151  	if err != nil {
   152  		return 0, err
   153  	}
   154  
   155  	// Quick check
   156  	if v1 == v2 {
   157  		return 0, nil
   158  	}
   159  
   160  	// Max/Min comparison
   161  	if v1 == minVersion || v2 == maxVersion {
   162  		return -1, nil
   163  	}
   164  	if v2 == minVersion || v1 == maxVersion {
   165  		return 1, nil
   166  	}
   167  
   168  	// Compare epochs
   169  	if v1.epoch > v2.epoch {
   170  		return 1, nil
   171  	}
   172  	if v1.epoch < v2.epoch {
   173  		return -1, nil
   174  	}
   175  
   176  	// Compare version
   177  	rc := rpmvercmp(v1.version, v2.version)
   178  	if rc != 0 {
   179  		return rc, nil
   180  	}
   181  
   182  	// Compare revision
   183  	return rpmvercmp(v1.release, v2.release), nil
   184  }
   185  
   186  // rpmcmpver compares two version or release strings.
   187  //
   188  // Lifted from github.com/cavaliercoder/go-rpm.
   189  // For the original C implementation, see:
   190  // https://github.com/rpm-software-management/rpm/blob/master/lib/rpmvercmp.c#L16
   191  func rpmvercmp(strA, strB string) int {
   192  	// shortcut for equality
   193  	if strA == strB {
   194  		return 0
   195  	}
   196  
   197  	// get alpha/numeric segements
   198  	segsa := alphanumPattern.FindAllString(strA, -1)
   199  	segsb := alphanumPattern.FindAllString(strB, -1)
   200  	segs := int(math.Min(float64(len(segsa)), float64(len(segsb))))
   201  
   202  	// compare each segment
   203  	for i := 0; i < segs; i++ {
   204  		a := segsa[i]
   205  		b := segsb[i]
   206  
   207  		// compare tildes
   208  		if []rune(a)[0] == '~' && []rune(b)[0] == '~' {
   209  			continue
   210  		}
   211  		if []rune(a)[0] == '~' && []rune(b)[0] != '~' {
   212  			return -1
   213  		}
   214  		if []rune(a)[0] != '~' && []rune(b)[0] == '~' {
   215  			return 1
   216  		}
   217  
   218  		if unicode.IsNumber([]rune(a)[0]) {
   219  			// numbers are always greater than alphas
   220  			if !unicode.IsNumber([]rune(b)[0]) {
   221  				// a is numeric, b is alpha
   222  				return 1
   223  			}
   224  
   225  			// trim leading zeros
   226  			a = strings.TrimLeft(a, "0")
   227  			b = strings.TrimLeft(b, "0")
   228  
   229  			// longest string wins without further comparison
   230  			if len(a) > len(b) {
   231  				return 1
   232  			} else if len(b) > len(a) {
   233  				return -1
   234  			}
   235  		} else if unicode.IsNumber([]rune(b)[0]) {
   236  			// a is alpha, b is numeric
   237  			return -1
   238  		}
   239  
   240  		// string compare
   241  		if a < b {
   242  			return -1
   243  		} else if a > b {
   244  			return 1
   245  		}
   246  	}
   247  
   248  	// segments were all the same but separators must have been different
   249  	if len(segsa) == len(segsb) {
   250  		return 0
   251  	}
   252  
   253  	// If there is a tilde in a segment past the min number of segments, find it.
   254  	if len(segsa) > segs && []rune(segsa[segs])[0] == '~' {
   255  		return -1
   256  	} else if len(segsb) > segs && []rune(segsb[segs])[0] == '~' {
   257  		return 1
   258  	}
   259  
   260  	// whoever has the most segments wins
   261  	if len(segsa) > len(segsb) {
   262  		return 1
   263  	}
   264  
   265  	return -1
   266  }
   267  
   268  // String returns the string representation of a Version.
   269  func (v version) String() (s string) {
   270  	if v.epoch != 0 {
   271  		s = strconv.Itoa(v.epoch) + ":"
   272  	}
   273  	s += v.version
   274  	if v.release != "" {
   275  		s += "-" + v.release
   276  	}
   277  	return
   278  }
   279  
   280  func validSymbol(r rune) bool {
   281  	return containsRune(allowedSymbols, r)
   282  }
   283  
   284  func containsRune(s []rune, e rune) bool {
   285  	for _, a := range s {
   286  		if a == e {
   287  			return true
   288  		}
   289  	}
   290  	return false
   291  }