github.com/quay/claircore@v1.5.28/java/maven_version.go (about)

     1  package java
     2  
     3  import (
     4  	"fmt"
     5  	"math/big"
     6  	"strconv"
     7  	"strings"
     8  	"unicode"
     9  )
    10  
    11  // See https://cwiki.apache.org/confluence/display/MAVENOLD/Versioning
    12  //
    13  // Maven versions seem to have the extremely fun property of being arbitrarily long
    14  // and arbitrarily nested. The wiki reference is also incorrect -- the comparison
    15  // function is reverse-engineered from actual behavior rather than specified
    16  // behavior.
    17  //
    18  // See also:
    19  // https://github.com/apache/maven/blob/maven-3.9.x/maven-artifact/src/main/java/org/apache/maven/artifact/versioning/ComparableVersion.java
    20  
    21  type mavenVersion struct {
    22  	Orig *string
    23  	C    component
    24  }
    25  
    26  type component struct {
    27  	Str  *string
    28  	Int  *big.Int // There's no size on a numeric component, because that'd be too easy.
    29  	List []component
    30  }
    31  
    32  const (
    33  	nullComponent = iota
    34  	intComponent
    35  	stringComponent
    36  	listComponent
    37  )
    38  
    39  func (c *component) String() string {
    40  	if c == nil {
    41  		return "<nil>"
    42  	}
    43  	switch c.Kind() {
    44  	case intComponent:
    45  		return c.Int.Text(10)
    46  	case stringComponent:
    47  		return strconv.Quote(*c.Str)
    48  	case listComponent:
    49  		var b strings.Builder
    50  		b.WriteByte('[')
    51  		for i := range c.List {
    52  			if i != 0 {
    53  				b.WriteByte(',')
    54  			}
    55  			b.WriteString(c.List[i].String())
    56  		}
    57  		b.WriteByte(']')
    58  		return b.String()
    59  	default:
    60  	}
    61  	return "null"
    62  }
    63  func (c *component) Kind() int {
    64  	switch {
    65  	case c.Int != nil:
    66  		return intComponent
    67  	case c.Str != nil:
    68  		return stringComponent
    69  	case c.List != nil:
    70  		return listComponent
    71  	default:
    72  		return nullComponent
    73  	}
    74  }
    75  
    76  func (a *component) Compare(b *component) int {
    77  	// The maven version algorithm has the curious property of explicitly
    78  	// allowing comparisons to `nil`.
    79  	switch {
    80  	case a == nil:
    81  		panic("programmer error: Compare called with nil receiver")
    82  	case a.Kind() == intComponent && b == nil:
    83  		b = &component{Int: zero}
    84  		fallthrough
    85  	case a.Kind() == intComponent && b.Kind() == intComponent:
    86  		return a.Int.Cmp(b.Int)
    87  	case a.Kind() == intComponent && b.Kind() == listComponent:
    88  		return 1
    89  	case a.Kind() == intComponent && b.Kind() == stringComponent:
    90  		return 1
    91  	case a.Kind() == listComponent && b == nil:
    92  		if len(a.List) == 0 {
    93  			return 0
    94  		}
    95  		for i := range a.List {
    96  			c := a.List[i].Compare(nil)
    97  			if c != 0 {
    98  				return c
    99  			}
   100  		}
   101  		return 0
   102  	case a.Kind() == listComponent && b.Kind() == listComponent:
   103  		for i := 0; i < len(a.List) || i < len(b.List); i++ {
   104  			var l, r *component
   105  			if i < len(a.List) {
   106  				l = &a.List[i]
   107  			}
   108  			if i < len(b.List) {
   109  				r = &b.List[i]
   110  			}
   111  			var res int
   112  			if l == nil {
   113  				if r != nil {
   114  					res = -1 * r.Compare(l)
   115  				}
   116  			} else {
   117  				res = l.Compare(r)
   118  			}
   119  			if res != 0 {
   120  				return res
   121  			}
   122  		}
   123  		return 0
   124  	case a.Kind() == listComponent && b.Kind() == intComponent:
   125  		return -1
   126  	case a.Kind() == listComponent && b.Kind() == stringComponent:
   127  		return 1
   128  	case a.Kind() == stringComponent && b == nil:
   129  		b = &component{Str: new(string)}
   130  		fallthrough
   131  	case a.Kind() == stringComponent && b.Kind() == stringComponent:
   132  		return strings.Compare(ordString(*a.Str), ordString(*b.Str))
   133  	case a.Kind() == stringComponent && b.Kind() == intComponent:
   134  		return -1
   135  	case a.Kind() == stringComponent && b.Kind() == listComponent:
   136  		return -1
   137  	default:
   138  		panic("programmer error: unhandled logic possibility")
   139  	}
   140  }
   141  
   142  func parseMavenVersion(s string) (*mavenVersion, error) {
   143  	v := &mavenVersion{
   144  		Orig: &s,
   145  	}
   146  	var b strings.Builder
   147  	l := &v.C.List
   148  	isDigit := false
   149  	pos := 0
   150  	for i, r := range s {
   151  		switch {
   152  		case r == '.':
   153  			if i == pos {
   154  				b.WriteByte('0')
   155  			}
   156  			if isDigit {
   157  				if err := appendInt(l, &b); err != nil {
   158  					return nil, err
   159  				}
   160  			} else {
   161  				appendString(l, &b)
   162  			}
   163  			pos = i + 1
   164  		case r == '-':
   165  			if i == pos {
   166  				b.WriteByte('0')
   167  			}
   168  			if isDigit {
   169  				if err := appendInt(l, &b); err != nil {
   170  					return nil, err
   171  				}
   172  			} else {
   173  				appendString(l, &b)
   174  			}
   175  			l = appendList(l)
   176  			pos = i + 1
   177  		case unicode.IsDigit(r):
   178  			if !isDigit && i > pos {
   179  				appendString(l, &b)
   180  				l = appendList(l)
   181  				pos = i
   182  			}
   183  			isDigit = true
   184  			b.WriteRune(r)
   185  		default:
   186  			if isDigit && i > pos {
   187  				if err := appendInt(l, &b); err != nil {
   188  					return nil, err
   189  				}
   190  				l = appendList(l)
   191  				pos = i
   192  			}
   193  			isDigit = false
   194  			b.WriteRune(r)
   195  		}
   196  	}
   197  	if isDigit {
   198  		if err := appendInt(l, &b); err != nil {
   199  			return nil, err
   200  		}
   201  	} else {
   202  		appendString(l, &b)
   203  	}
   204  	normalize(&v.C.List)
   205  	return v, nil
   206  }
   207  
   208  func appendInt(l *[]component, b *strings.Builder) error {
   209  	var v big.Int
   210  	if _, ok := v.SetString(b.String(), 10); !ok {
   211  		return fmt.Errorf("unable to parse number %q", b.String())
   212  	}
   213  	*l = append(*l, component{Int: &v})
   214  	b.Reset()
   215  	return nil
   216  }
   217  
   218  func appendString(l *[]component, b *strings.Builder) error {
   219  	s := b.String()
   220  	*l = append(*l, component{Str: &s})
   221  	b.Reset()
   222  	return nil
   223  }
   224  
   225  func appendList(l *[]component) *[]component {
   226  	ci := len(*l)
   227  	*l = append(*l, component{})
   228  	c := &(*l)[ci]
   229  	return &c.List
   230  }
   231  
   232  var zero = big.NewInt(0)
   233  
   234  func (c *component) isNull() bool {
   235  	return c == nil ||
   236  		(c.Int != nil && c.Int.Cmp(zero) == 0) ||
   237  		(c.Str != nil && *c.Str == "") ||
   238  		(c.List != nil && len(c.List) == 0)
   239  }
   240  
   241  func normalize(cs *[]component) {
   242  	for i := len(*cs) - 1; i >= 0; i-- {
   243  		c := &(*cs)[i]
   244  		if c.isNull() {
   245  			j := i + 1
   246  			if j > len(*cs) {
   247  				*cs = (*cs)[:i]
   248  			} else {
   249  				*cs = append((*cs)[:i], (*cs)[j:]...)
   250  			}
   251  			continue
   252  		} else if c.Kind() != listComponent {
   253  			break
   254  		}
   255  		normalize(&c.List)
   256  	}
   257  }
   258  
   259  // Compare is the standard comparison function: < == -1, == == 0, > == 1.
   260  func (v *mavenVersion) Compare(v2 *mavenVersion) int {
   261  	return v.C.Compare(&v2.C)
   262  }
   263  
   264  // This is the maven string ordering function.
   265  //
   266  // Takes a string and returns a new string that sorts "properly" lexically.
   267  func ordString(s string) string {
   268  	s = strings.ToLower(s)
   269  	q, ok := qualifiers[s]
   270  	if !ok {
   271  		return fmt.Sprintf("%d-%s", unknownQualifier, s)
   272  	}
   273  	return q
   274  }
   275  
   276  // Qualifiers are reverse-engineered from the maven source:
   277  // https://github.com/apache/maven/blob/maven-3.9.x/maven-artifact/src/main/java/org/apache/maven/artifact/versioning/ComparableVersion.java#L356
   278  var qualifiers = map[string]string{
   279  	"alpha":     "0",
   280  	"a":         "0",
   281  	"beta":      "1",
   282  	"b":         "1",
   283  	"milestone": "2",
   284  	"m":         "2",
   285  	"rc":        "3",
   286  	"cr":        "3",
   287  	"snapshot":  "4",
   288  	"":          "5",
   289  	"ga":        "5",
   290  	"final":     "5",
   291  	"release":   "5",
   292  	"sp":        "6",
   293  }
   294  
   295  // UnknownQualifier is prepended to arbitrary strings in ordString. This value
   296  // means arbitrary qualifiers sort after all known qualifiers, and lexically
   297  // within the set of unknown qualifiers.
   298  const unknownQualifier = 7