github.com/google/osv-scalibr@v0.4.1/semantic/version-alpine.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  	"regexp"
    20  	"strings"
    21  )
    22  
    23  var (
    24  	alpineNumberComponentsFinder     = regexp.MustCompile(`^((\d+)\.?)*`)
    25  	alpineIsFirstCharLowercaseLetter = regexp.MustCompile(`^[a-z]`)
    26  	alpineSuffixesFinder             = regexp.MustCompile(`_(alpha|beta|pre|rc|cvs|svn|git|hg|p)(\d*)`)
    27  	alpineHashFinder                 = regexp.MustCompile(`^~([0-9a-f]+)`)
    28  	alpineBuildComponentFinder       = regexp.MustCompile(`^-r(\d*)`)
    29  )
    30  
    31  type alpineNumberComponent struct {
    32  	original string
    33  	value    *big.Int
    34  	index    int
    35  }
    36  
    37  func (anc alpineNumberComponent) Cmp(b alpineNumberComponent) int {
    38  	// ignore trailing zeros for the first digits in each version
    39  	if anc.index != 0 && b.index != 0 {
    40  		if anc.original[0] == '0' || b.original[0] == '0' {
    41  			return strings.Compare(anc.original, b.original)
    42  		}
    43  	}
    44  
    45  	return anc.value.Cmp(b.value)
    46  }
    47  
    48  type alpineNumberComponents []alpineNumberComponent
    49  
    50  func (components *alpineNumberComponents) Fetch(n int) alpineNumberComponent {
    51  	if len(*components) <= n {
    52  		return alpineNumberComponent{original: "0", value: new(big.Int)}
    53  	}
    54  
    55  	return (*components)[n]
    56  }
    57  
    58  type alpineSuffix struct {
    59  	// the weight of this suffix for sorting, and implicitly what actual string it is:
    60  	//   *alpha*, *beta*, *pre*, *rc*, <no suffix>, *cvs*, *svn*, *git*, *hg*, *p*
    61  	weight int
    62  	// the number value of this suffix component
    63  	number *big.Int
    64  }
    65  
    66  // weights the given suffix string based on the sort order of official supported suffixes.
    67  //
    68  // this is expected to be _just_ the suffix "string" i.e. it should not start with a "_"
    69  // or have any trailing numbers.
    70  func weightAlpineSuffixString(suffixStr string) int {
    71  	// "p" is omitted since it's the highest suffix, so it will be the final return
    72  	supported := []string{"alpha", "beta", "pre", "rc", "", "cvs", "svn", "git", "hg"}
    73  
    74  	for i, s := range supported {
    75  		if suffixStr == s {
    76  			return i
    77  		}
    78  	}
    79  
    80  	// if we didn't match a support suffix already, then we're "p" which
    81  	// has the highest weight as our parser only captures valid suffixes
    82  	return len(supported)
    83  }
    84  
    85  // alpineVersion represents a version of an Alpine package.
    86  //
    87  // Currently, the APK version specification is as follows:
    88  // *number{.number}...{letter}{\_suffix{number}}...{~hash}{-r#}*
    89  //
    90  // Each *number* component is a sequence of digits (0-9).
    91  //
    92  // The *letter* portion can follow only after end of all the numeric
    93  // version components. The *letter* is a single lower case letter (a-z).
    94  // This can follow one or more *\_suffix{number}* components. The list
    95  // of valid suffixes (and their sorting order) is:
    96  // *alpha*, *beta*, *pre*, *rc*, <no suffix>, *cvs*, *svn*, *git*, *hg*, *p*
    97  //
    98  // This can be follows with an optional *{~hash}* to indicate a commit
    99  // hash from where it was built. This can be any length string of
   100  // lower case hexadecimal digits (0-9a-f).
   101  //
   102  // Finally, an optional package build component *-r{number}* can follow.
   103  //
   104  // Also see https://github.com/alpinelinux/apk-tools/blob/master/doc/apk-package.5.scd#package-info-metadata
   105  type alpineVersion struct {
   106  	// the original string that was parsed
   107  	original string
   108  	// whether the version was found to be invalid while parsing
   109  	invalid bool
   110  	// the remainder of the string after parsing has been completed
   111  	remainder string
   112  	// slice of number components which can be compared in a semver-like manner
   113  	components alpineNumberComponents
   114  	// optional single lower-case letter
   115  	letter string
   116  	// slice of one or more suffixes, prefixed with "_" and optionally followed by a number.
   117  	//
   118  	// supported suffixes and their sort order are:
   119  	//	*alpha*, *beta*, *pre*, *rc*, <no suffix>, *cvs*, *svn*, *git*, *hg*, *p*
   120  	suffixes []alpineSuffix
   121  	// optional commit hash made up of any number of lower case hexadecimal digits (0-9a-f)
   122  	hash string
   123  	// prefixed with "-r{number}"
   124  	buildComponent *big.Int
   125  }
   126  
   127  func (v alpineVersion) compareComponents(w alpineVersion) int {
   128  	numberOfComponents := max(len(v.components), len(w.components))
   129  
   130  	for i := range numberOfComponents {
   131  		diff := v.components.Fetch(i).Cmp(w.components.Fetch(i))
   132  
   133  		if diff != 0 {
   134  			return diff
   135  		}
   136  	}
   137  
   138  	return 0
   139  }
   140  
   141  func (v alpineVersion) compareLetters(w alpineVersion) int {
   142  	if v.letter == "" && w.letter != "" {
   143  		return -1
   144  	}
   145  	if v.letter != "" && w.letter == "" {
   146  		return +1
   147  	}
   148  
   149  	return strings.Compare(v.letter, w.letter)
   150  }
   151  
   152  func (v alpineVersion) fetchSuffix(n int) alpineSuffix {
   153  	if len(v.suffixes) <= n {
   154  		return alpineSuffix{number: big.NewInt(0), weight: 5}
   155  	}
   156  
   157  	return v.suffixes[n]
   158  }
   159  
   160  func (as alpineSuffix) Cmp(bs alpineSuffix) int {
   161  	if as.weight > bs.weight {
   162  		return +1
   163  	}
   164  	if as.weight < bs.weight {
   165  		return -1
   166  	}
   167  
   168  	return as.number.Cmp(bs.number)
   169  }
   170  
   171  func (v alpineVersion) compareSuffixes(w alpineVersion) int {
   172  	numberOfSuffixes := max(len(v.suffixes), len(w.suffixes))
   173  
   174  	for i := range numberOfSuffixes {
   175  		diff := v.fetchSuffix(i).Cmp(w.fetchSuffix(i))
   176  
   177  		if diff != 0 {
   178  			return diff
   179  		}
   180  	}
   181  
   182  	return 0
   183  }
   184  
   185  func (v alpineVersion) compareBuildComponents(w alpineVersion) int {
   186  	if v.buildComponent != nil && w.buildComponent != nil {
   187  		if diff := v.buildComponent.Cmp(w.buildComponent); diff != 0 {
   188  			return diff
   189  		}
   190  	}
   191  
   192  	return 0
   193  }
   194  
   195  func (v alpineVersion) compareRemainder(w alpineVersion) int {
   196  	if v.remainder == "" && w.remainder != "" {
   197  		return +1
   198  	}
   199  
   200  	if v.remainder != "" && w.remainder == "" {
   201  		return -1
   202  	}
   203  
   204  	return 0
   205  }
   206  
   207  func (v alpineVersion) compare(w alpineVersion) int {
   208  	// if both versions are invalid, then just use a string compare
   209  	if v.invalid && w.invalid {
   210  		return strings.Compare(v.original, w.original)
   211  	}
   212  
   213  	// note: commit hashes are ignored as we can't properly compare them
   214  	if diff := v.compareComponents(w); diff != 0 {
   215  		return diff
   216  	}
   217  	if diff := v.compareLetters(w); diff != 0 {
   218  		return diff
   219  	}
   220  	if diff := v.compareSuffixes(w); diff != 0 {
   221  		return diff
   222  	}
   223  	if diff := v.compareBuildComponents(w); diff != 0 {
   224  		return diff
   225  	}
   226  	if diff := v.compareRemainder(w); diff != 0 {
   227  		return diff
   228  	}
   229  
   230  	return 0
   231  }
   232  
   233  func (v alpineVersion) CompareStr(str string) (int, error) {
   234  	w, err := parseAlpineVersion(str)
   235  
   236  	if err != nil {
   237  		return 0, err
   238  	}
   239  
   240  	return v.compare(w), nil
   241  }
   242  
   243  // parseAlpineNumberComponents parses the given string into alpineVersion.components
   244  // and then returns the remainder of the string for continued parsing.
   245  //
   246  // Each number component is a sequence of digits (0-9), separated with a ".",
   247  // and with no limit on the value or amount of number components.
   248  //
   249  // This parser must be applied *before* any other parser.
   250  func parseAlpineNumberComponents(v *alpineVersion, str string) (string, error) {
   251  	sub := alpineNumberComponentsFinder.FindString(str)
   252  
   253  	if sub == "" {
   254  		return str, nil
   255  	}
   256  
   257  	for i, d := range strings.Split(sub, ".") {
   258  		// while technically not allowed by the spec, currently apk does not
   259  		// consider it invalid to have a dot that isn't followed by a digit
   260  		if d == "" {
   261  			break
   262  		}
   263  
   264  		value, err := convertToBigInt(d)
   265  
   266  		if err != nil {
   267  			return "", err
   268  		}
   269  
   270  		v.components = append(v.components, alpineNumberComponent{
   271  			value:    value,
   272  			index:    i,
   273  			original: d,
   274  		})
   275  	}
   276  
   277  	return strings.TrimPrefix(str, sub), nil
   278  }
   279  
   280  // parseAlpineLetter parses the given string into an alpineVersion.letter
   281  // and then returns the remainder of the string for continued parsing.
   282  //
   283  // The letter is optional, following after the numeric version components, and
   284  // must be a single lower case letter (a-z).
   285  //
   286  // This parser must be applied *after* parseAlpineNumberComponents.
   287  func parseAlpineLetter(v *alpineVersion, str string) string {
   288  	if alpineIsFirstCharLowercaseLetter.MatchString(str) {
   289  		v.letter = str[:1]
   290  	}
   291  
   292  	return strings.TrimPrefix(str, v.letter)
   293  }
   294  
   295  // parseAlpineSuffixes parses the given string into alpineVersion.suffixes and
   296  // then returns the remainder of the string for continued parsing.
   297  //
   298  // Suffixes begin with an "_" and may optionally end with a number.
   299  //
   300  // This parser must be applied *after* parseAlpineLetter.
   301  func parseAlpineSuffixes(v *alpineVersion, str string) (string, error) {
   302  	for _, match := range alpineSuffixesFinder.FindAllStringSubmatch(str, -1) {
   303  		if match[2] == "" {
   304  			match[2] = "0"
   305  		}
   306  
   307  		number, err := convertToBigInt(match[2])
   308  
   309  		if err != nil {
   310  			return "", err
   311  		}
   312  
   313  		v.suffixes = append(v.suffixes, alpineSuffix{
   314  			weight: weightAlpineSuffixString(match[1]),
   315  			number: number,
   316  		})
   317  		str = strings.TrimPrefix(str, match[0])
   318  	}
   319  
   320  	return str, nil
   321  }
   322  
   323  // parseAlpineHash parses the given string into alpineVersion.hash and then returns
   324  // the remainder of the string for continued parsing.
   325  //
   326  // The hash is an optional value representing a commit hash, which is a string of
   327  // that starts with a "~" and is followed by any number of lower case hexadecimal
   328  // digits (0-9a-f).
   329  //
   330  // This parser must be applied *after* parseAlpineSuffixes.
   331  func parseAlpineHash(v *alpineVersion, str string) string {
   332  	v.hash = alpineHashFinder.FindString(str)
   333  
   334  	return strings.TrimPrefix(str, v.hash)
   335  }
   336  
   337  // parseAlpineBuildComponent parses the given string into alpineVersion.buildComponent
   338  // and then returns the remainder of the string for continued parsing.
   339  //
   340  // The build component is an optional value at the end of the version string which
   341  // begins with "-r" followed by a number.
   342  //
   343  // This parser must be applied *after* parseAlpineBuildComponent
   344  func parseAlpineBuildComponent(v *alpineVersion, str string) (string, error) {
   345  	if str == "" {
   346  		return str, nil
   347  	}
   348  
   349  	matches := alpineBuildComponentFinder.FindStringSubmatch(str)
   350  
   351  	if matches == nil {
   352  		// since this is the last part of parsing, anything other than an empty string
   353  		// must match as a build component or otherwise the version is invalid
   354  		v.invalid = true
   355  
   356  		return str, nil
   357  	}
   358  
   359  	if matches[1] == "" {
   360  		matches[1] = "0"
   361  	}
   362  
   363  	buildComponent, err := convertToBigInt(matches[1])
   364  
   365  	if err != nil {
   366  		return "", err
   367  	}
   368  
   369  	v.buildComponent = buildComponent
   370  
   371  	return strings.TrimPrefix(str, matches[0]), nil
   372  }
   373  
   374  func parseAlpineVersion(str string) (alpineVersion, error) {
   375  	var err error
   376  
   377  	v := alpineVersion{original: str, buildComponent: new(big.Int)}
   378  
   379  	if str, err = parseAlpineNumberComponents(&v, str); err != nil {
   380  		return alpineVersion{}, err
   381  	}
   382  
   383  	str = parseAlpineLetter(&v, str)
   384  
   385  	if str, err = parseAlpineSuffixes(&v, str); err != nil {
   386  		return alpineVersion{}, err
   387  	}
   388  
   389  	str = parseAlpineHash(&v, str)
   390  
   391  	if str, err = parseAlpineBuildComponent(&v, str); err != nil {
   392  		return alpineVersion{}, err
   393  	}
   394  
   395  	v.remainder = str
   396  
   397  	return v, nil
   398  }