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