github.com/quay/claircore@v1.5.28/ruby/version.go (about)

     1  package ruby
     2  
     3  import (
     4  	"errors"
     5  	"regexp"
     6  	"strings"
     7  )
     8  
     9  var (
    10  	anchoredVersion = regexp.MustCompile(`^\s*([0-9]+(\.[0-9a-zA-Z]+)*(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?)?\s*$`)
    11  
    12  	errInvalidVersion = errors.New("invalid gem version")
    13  )
    14  
    15  // Version is a RubyGem version.
    16  // This is based on the official [implementation].
    17  //
    18  // [implementation]: https://github.com/rubygems/rubygems/blob/1a1948424aca90013b37cb8a78f5d1d5576023f1/lib/rubygems/version.rb
    19  type Version struct {
    20  	segs []Segment
    21  }
    22  
    23  // NewVersion creates a Version out of the given string.
    24  func NewVersion(version string) (Version, error) {
    25  	if !anchoredVersion.MatchString(version) {
    26  		return Version{}, errInvalidVersion
    27  	}
    28  
    29  	version = strings.TrimSpace(version)
    30  	if version == "" {
    31  		version = "0"
    32  	}
    33  	version = strings.ReplaceAll(version, "-", ".pre.")
    34  
    35  	return Version{
    36  		segs: canonicalize(version),
    37  	}, nil
    38  }
    39  
    40  // Compare returns an integer comparing this version, v, to other.
    41  // The result will be 0 if v == other, -1 if v < other, and +1 if v > other.
    42  func (v Version) Compare(other Version) int {
    43  	segs := v.segs
    44  	otherSegs := other.segs
    45  
    46  	leftLen := len(segs)
    47  	rightLen := len(otherSegs)
    48  	limit := max(leftLen, rightLen)
    49  	for i := 0; i < limit; i++ {
    50  		left, right := Segment(numericSegment("0")), Segment(numericSegment("0"))
    51  		if i < leftLen {
    52  			left = segs[i]
    53  		}
    54  		if i < rightLen {
    55  			right = otherSegs[i]
    56  		}
    57  
    58  		if cmp := left.Compare(right); cmp != 0 {
    59  			return cmp
    60  		}
    61  	}
    62  
    63  	return 0
    64  }
    65  
    66  func canonicalize(v string) []Segment {
    67  	segs, prerelease := partitionSegments(v)
    68  
    69  	// Remove trailing zero segments.
    70  	i := len(segs) - 1
    71  	for ; i >= 0; i-- {
    72  		seg, ok := segs[i].(numericSegment)
    73  		if !ok || !seg.isZero() {
    74  			break
    75  		}
    76  	}
    77  	segs = segs[:i+1]
    78  
    79  	// Remove all zero segments preceding the first letter in a prerelease version.
    80  	if prerelease {
    81  		// Find the first letter in the version.
    82  		end := -1
    83  		for i := 0; i < len(segs); i++ {
    84  			if _, ok := segs[i].(stringSegment); ok {
    85  				end = i
    86  				break
    87  			}
    88  		}
    89  		if end != -1 {
    90  			// Find where the preceding zeroes start.
    91  			var start int
    92  			for i := end - 1; i >= 0; i-- {
    93  				seg, ok := segs[i].(numericSegment)
    94  				if !ok || !seg.isZero() {
    95  					start = i + 1
    96  					break
    97  				}
    98  			}
    99  			segs = append(segs[:start], segs[end:]...)
   100  		}
   101  	}
   102  
   103  	return segs
   104  }
   105  
   106  func partitionSegments(v string) ([]Segment, bool) {
   107  	var prerelease bool
   108  	splitVersion := strings.Split(v, ".")
   109  	segs := make([]Segment, 0, len(splitVersion))
   110  	for _, s := range splitVersion {
   111  		if s == "" {
   112  			continue
   113  		}
   114  
   115  		if onlyDigits(s) {
   116  			segs = append(segs, numericSegment(s))
   117  			continue
   118  		}
   119  
   120  		// Ruby considers any version with a letter to be a prerelease.
   121  		prerelease = true
   122  		segs = append(segs, stringSegment(s))
   123  	}
   124  
   125  	return segs, prerelease
   126  }
   127  
   128  func onlyDigits(s string) bool {
   129  	// I don't know if converting to a []byte does anything
   130  	// special here, but [strconv.ParseUint] does it when ranging over a string,
   131  	// and this implementation is based on code from [strconv.ParseUint].
   132  	for _, c := range []byte(s) {
   133  		if c < '0' || c > '9' {
   134  			return false
   135  		}
   136  	}
   137  	return true
   138  }
   139  
   140  func max(a, b int) int {
   141  	if b > a {
   142  		return b
   143  	}
   144  	return a
   145  }
   146  
   147  // Segment is a part of a RubyGem version.
   148  type Segment interface {
   149  	Compare(other Segment) int
   150  }
   151  
   152  var (
   153  	_ Segment = (*stringSegment)(nil)
   154  	_ Segment = (*numericSegment)(nil)
   155  )
   156  
   157  type stringSegment string
   158  
   159  // Compare returns an integer comparing this version, s, to other.
   160  // The result will be 0 if v == other, -1 if v < other, and +1 if v > other.
   161  //
   162  // A stringSegment is always less than a numericSegment.
   163  func (s stringSegment) Compare(other Segment) int {
   164  	switch seg := other.(type) {
   165  	case numericSegment:
   166  		return -1
   167  	case stringSegment:
   168  		return strings.Compare(string(s), string(seg))
   169  	default:
   170  		panic("Programmer error")
   171  	}
   172  }
   173  
   174  type numericSegment string
   175  
   176  // Compare returns an integer comparing this version, n, to other.
   177  // The result will be 0 if n == other, -1 if n < other, and +1 if n > other.
   178  //
   179  // A numericSegment is always greater than a stringSegment.
   180  func (n numericSegment) Compare(other Segment) int {
   181  	switch seg := other.(type) {
   182  	case stringSegment:
   183  		return +1
   184  	case numericSegment:
   185  		left, leftLen := string(n), len(n)
   186  		right, rightLen := string(seg), len(seg)
   187  		// The length of each string must match to compare them properly.
   188  		// Pad the shorter string with zeroes.
   189  		if leftLen == max(leftLen, rightLen) {
   190  			right = strings.Repeat("0", leftLen-rightLen) + right
   191  		} else {
   192  			left = strings.Repeat("0", rightLen-leftLen) + left
   193  		}
   194  		return strings.Compare(left, right)
   195  	default:
   196  		panic("Programmer error")
   197  	}
   198  }
   199  
   200  func (n numericSegment) isZero() bool {
   201  	// Again, I don't know if converting to a []byte does anything
   202  	// special here, but [strconv.ParseUint] does it when ranging over a string,
   203  	// and this implementation is based on code from [strconv.ParseUint].
   204  	for _, c := range []byte(n) {
   205  		if c != '0' {
   206  			return false
   207  		}
   208  	}
   209  	return true
   210  }