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

     1  // Package pep440 implements types for working with versions as defined in
     2  // PEP-440.
     3  package pep440
     4  
     5  import (
     6  	"fmt"
     7  	"regexp"
     8  	"strconv"
     9  	"strings"
    10  
    11  	"github.com/quay/claircore"
    12  )
    13  
    14  var pattern *regexp.Regexp
    15  
    16  func init() {
    17  	// This is the regexp used in the "versioning" package, as noted in
    18  	// https://www.python.org/dev/peps/pep-0440/#id81
    19  	const r = `v?` +
    20  		`(?:` +
    21  		`(?:(?P<epoch>[0-9]+)!)?` + // epoch
    22  		`(?P<release>[0-9]+(?:\.[0-9]+)*)` + // release segment
    23  		`(?P<pre>[-_\.]?(?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))[-_\.]?(?P<pre_n>[0-9]+)?)?` + // pre release
    24  		`(?P<post>(?:-(?P<post_n1>[0-9]+))|(?:[-_\.]?(?P<post_l>post|rev|r)[-_\.]?(?P<post_n2>[0-9]+)?))?` + // post release
    25  		`(?P<dev>[-_\.]?(?P<dev_l>dev)[-_\.]?(?P<dev_n>[0-9]+)?)?` + // dev release
    26  		`)` +
    27  		`(?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?` // local version
    28  	pattern = regexp.MustCompile(r)
    29  }
    30  
    31  // Version repesents a canonical-ish represention of a PEP440 version.
    32  //
    33  // Local revisions are discarded.
    34  type Version struct {
    35  	Epoch   int
    36  	Release []int
    37  	Pre     struct {
    38  		Label string
    39  		N     int
    40  	}
    41  	Post int
    42  	Dev  int
    43  }
    44  
    45  // Version returns a fixed-width slice of integers meant for allowing some
    46  // amount of version comparision with no knowledge of the version scheme.
    47  //
    48  // In generating this slice, the following rules are applied:
    49  //
    50  // Release is normalized to five numbers. Missing numbers are set to "0" and
    51  // additional numbers are dropped.
    52  //
    53  // The Dev revision is promoted earlier in the int slice if there's no Pre or
    54  // Post revision, and sorts as earlier than a Pre revision.
    55  func (v *Version) Version() (c claircore.Version) {
    56  	const (
    57  		epoch = 0
    58  		rel   = 1
    59  		preL  = 6
    60  		preN  = 7
    61  		post  = 8
    62  		dev   = 9
    63  	)
    64  	// BUG(hank) The int-slice versioning method tries to accomdate arbitrary
    65  	// numbers, but may give odd results with sufficiently large revision
    66  	// numbers. One suggested workaround is to make fewer than 9 quintillion
    67  	// releases.
    68  	c.Kind = "pep440"
    69  	c.V[epoch] = int32(v.Epoch)
    70  	for i, n := range v.Release {
    71  		if i > 4 {
    72  			break
    73  		}
    74  		c.V[rel+i] = int32(n)
    75  	}
    76  	switch v.Pre.Label {
    77  	case "a":
    78  		c.V[preL] = -3
    79  	case "b":
    80  		c.V[preL] = -2
    81  	case "rc":
    82  		c.V[preL] = -1
    83  	}
    84  	c.V[preN] = int32(v.Pre.N)
    85  	c.V[post] = int32(v.Post)
    86  	if v.Dev != 0 {
    87  		if v.Post != 0 || c.V[preL] != 0 {
    88  			c.V[dev] = -int32(v.Dev)
    89  		} else {
    90  			const minInt = -int32((^uint32(0))>>1) - 1
    91  			c.V[preL] = minInt + int32(v.Dev)
    92  		}
    93  	}
    94  
    95  	return c
    96  }
    97  
    98  // String returns the canonicalized representation of the Version.
    99  func (v *Version) String() string {
   100  	var b strings.Builder
   101  	if v.Epoch != 0 {
   102  		fmt.Fprintf(&b, "%d!", v.Epoch)
   103  	}
   104  	for i, n := range v.Release {
   105  		if i != 0 {
   106  			b.WriteByte('.')
   107  		}
   108  		b.WriteString(strconv.FormatInt(int64(n), 10))
   109  	}
   110  	if v.Pre.Label != "" {
   111  		b.WriteString(v.Pre.Label)
   112  		b.WriteString(strconv.FormatInt(int64(v.Pre.N), 10))
   113  	}
   114  	if v.Post != 0 {
   115  		fmt.Fprintf(&b, ".post%d", v.Post)
   116  	}
   117  	if v.Dev != 0 {
   118  		fmt.Fprintf(&b, ".dev%d", v.Dev)
   119  	}
   120  	return b.String()
   121  }
   122  
   123  // Compare returns an integer comparing two versions. The result will be 0 if
   124  // a == b, -1 if a < b and +1 if a > b.
   125  func (a *Version) Compare(b *Version) int {
   126  	av, bv := a.Version(), b.Version()
   127  	return av.Compare(&bv)
   128  }
   129  
   130  // Parse attempts to extract a PEP-440 version string from the provided string.
   131  func Parse(s string) (v Version, err error) {
   132  	if !pattern.MatchString(s) {
   133  		return v, fmt.Errorf("invalid pep440 version: %q", s)
   134  	}
   135  
   136  	ms := pattern.FindStringSubmatch(s)
   137  	for i, n := range pattern.SubexpNames() {
   138  		if ms[i] == "" {
   139  			continue
   140  		}
   141  
   142  		switch n {
   143  		case "epoch":
   144  			v.Epoch, err = strconv.Atoi(ms[i])
   145  			if err != nil {
   146  				return v, err
   147  			}
   148  		case "release":
   149  			ns := strings.Split(ms[i], ".")
   150  			v.Release = make([]int, len(ns))
   151  			for i, n := range ns {
   152  				v.Release[i], err = strconv.Atoi(n)
   153  				if err != nil {
   154  					return v, err
   155  				}
   156  			}
   157  		case "pre_l":
   158  			switch l := ms[i]; l {
   159  			case "a", "alpha":
   160  				v.Pre.Label = "a"
   161  			case "b", "beta":
   162  				v.Pre.Label = "b"
   163  			case "rc", "c", "pre", "preview":
   164  				v.Pre.Label = "rc"
   165  			default:
   166  				return v, fmt.Errorf("unknown pre-release label %q", l)
   167  			}
   168  		case "pre_n":
   169  			v.Pre.N, err = strconv.Atoi(ms[i])
   170  			if err != nil {
   171  				return v, err
   172  			}
   173  		case "post_n1", "post_n2":
   174  			v.Post, err = strconv.Atoi(ms[i])
   175  			if err != nil {
   176  				return v, err
   177  			}
   178  		case "dev_n":
   179  			v.Dev, err = strconv.Atoi(ms[i])
   180  			if err != nil {
   181  				return v, err
   182  			}
   183  		}
   184  	}
   185  
   186  	return v, nil
   187  }
   188  
   189  // Versions implements sort.Interface.
   190  type Versions []Version
   191  
   192  func (vs Versions) Len() int {
   193  	return len([]Version(vs))
   194  }
   195  
   196  func (vs Versions) Less(i, j int) bool {
   197  	return vs[i].Compare(&vs[j]) == -1
   198  }
   199  
   200  func (vs Versions) Swap(i, j int) {
   201  	vs[i], vs[j] = vs[j], vs[i]
   202  }