1package semver 2 3import ( 4 "errors" 5 "fmt" 6 "regexp" 7 "strings" 8) 9 10// Constraints is one or more constraint that a semantic version can be 11// checked against. 12type Constraints struct { 13 constraints [][]*constraint 14} 15 16// NewConstraint returns a Constraints instance that a Version instance can 17// be checked against. If there is a parse error it will be returned. 18func NewConstraint(c string) (*Constraints, error) { 19 20 // Rewrite - ranges into a comparison operation. 21 c = rewriteRange(c) 22 23 ors := strings.Split(c, "||") 24 or := make([][]*constraint, len(ors)) 25 for k, v := range ors { 26 cs := strings.Split(v, ",") 27 result := make([]*constraint, len(cs)) 28 for i, s := range cs { 29 pc, err := parseConstraint(s) 30 if err != nil { 31 return nil, err 32 } 33 34 result[i] = pc 35 } 36 or[k] = result 37 } 38 39 o := &Constraints{constraints: or} 40 return o, nil 41} 42 43// Check tests if a version satisfies the constraints. 44func (cs Constraints) Check(v *Version) bool { 45 // loop over the ORs and check the inner ANDs 46 for _, o := range cs.constraints { 47 joy := true 48 for _, c := range o { 49 if !c.check(v) { 50 joy = false 51 break 52 } 53 } 54 55 if joy { 56 return true 57 } 58 } 59 60 return false 61} 62 63// Validate checks if a version satisfies a constraint. If not a slice of 64// reasons for the failure are returned in addition to a bool. 65func (cs Constraints) Validate(v *Version) (bool, []error) { 66 // loop over the ORs and check the inner ANDs 67 var e []error 68 69 // Capture the prerelease message only once. When it happens the first time 70 // this var is marked 71 var prerelesase bool 72 for _, o := range cs.constraints { 73 joy := true 74 for _, c := range o { 75 // Before running the check handle the case there the version is 76 // a prerelease and the check is not searching for prereleases. 77 if c.con.pre == "" && v.pre != "" { 78 if !prerelesase { 79 em := fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) 80 e = append(e, em) 81 prerelesase = true 82 } 83 joy = false 84 85 } else { 86 87 if !c.check(v) { 88 em := fmt.Errorf(c.msg, v, c.orig) 89 e = append(e, em) 90 joy = false 91 } 92 } 93 } 94 95 if joy { 96 return true, []error{} 97 } 98 } 99 100 return false, e 101} 102 103var constraintOps map[string]cfunc 104var constraintMsg map[string]string 105var constraintRegex *regexp.Regexp 106 107func init() { 108 constraintOps = map[string]cfunc{ 109 "": constraintTildeOrEqual, 110 "=": constraintTildeOrEqual, 111 "!=": constraintNotEqual, 112 ">": constraintGreaterThan, 113 "<": constraintLessThan, 114 ">=": constraintGreaterThanEqual, 115 "=>": constraintGreaterThanEqual, 116 "<=": constraintLessThanEqual, 117 "=<": constraintLessThanEqual, 118 "~": constraintTilde, 119 "~>": constraintTilde, 120 "^": constraintCaret, 121 } 122 123 constraintMsg = map[string]string{ 124 "": "%s is not equal to %s", 125 "=": "%s is not equal to %s", 126 "!=": "%s is equal to %s", 127 ">": "%s is less than or equal to %s", 128 "<": "%s is greater than or equal to %s", 129 ">=": "%s is less than %s", 130 "=>": "%s is less than %s", 131 "<=": "%s is greater than %s", 132 "=<": "%s is greater than %s", 133 "~": "%s does not have same major and minor version as %s", 134 "~>": "%s does not have same major and minor version as %s", 135 "^": "%s does not have same major version as %s", 136 } 137 138 ops := make([]string, 0, len(constraintOps)) 139 for k := range constraintOps { 140 ops = append(ops, regexp.QuoteMeta(k)) 141 } 142 143 constraintRegex = regexp.MustCompile(fmt.Sprintf( 144 `^\s*(%s)\s*(%s)\s*$`, 145 strings.Join(ops, "|"), 146 cvRegex)) 147 148 constraintRangeRegex = regexp.MustCompile(fmt.Sprintf( 149 `\s*(%s)\s+-\s+(%s)\s*`, 150 cvRegex, cvRegex)) 151} 152 153// An individual constraint 154type constraint struct { 155 // The callback function for the restraint. It performs the logic for 156 // the constraint. 157 function cfunc 158 159 msg string 160 161 // The version used in the constraint check. For example, if a constraint 162 // is '<= 2.0.0' the con a version instance representing 2.0.0. 163 con *Version 164 165 // The original parsed version (e.g., 4.x from != 4.x) 166 orig string 167 168 // When an x is used as part of the version (e.g., 1.x) 169 minorDirty bool 170 dirty bool 171 patchDirty bool 172} 173 174// Check if a version meets the constraint 175func (c *constraint) check(v *Version) bool { 176 return c.function(v, c) 177} 178 179type cfunc func(v *Version, c *constraint) bool 180 181func parseConstraint(c string) (*constraint, error) { 182 m := constraintRegex.FindStringSubmatch(c) 183 if m == nil { 184 return nil, fmt.Errorf("improper constraint: %s", c) 185 } 186 187 ver := m[2] 188 orig := ver 189 minorDirty := false 190 patchDirty := false 191 dirty := false 192 if isX(m[3]) { 193 ver = "0.0.0" 194 dirty = true 195 } else if isX(strings.TrimPrefix(m[4], ".")) || m[4] == "" { 196 minorDirty = true 197 dirty = true 198 ver = fmt.Sprintf("%s.0.0%s", m[3], m[6]) 199 } else if isX(strings.TrimPrefix(m[5], ".")) { 200 dirty = true 201 patchDirty = true 202 ver = fmt.Sprintf("%s%s.0%s", m[3], m[4], m[6]) 203 } 204 205 con, err := NewVersion(ver) 206 if err != nil { 207 208 // The constraintRegex should catch any regex parsing errors. So, 209 // we should never get here. 210 return nil, errors.New("constraint Parser Error") 211 } 212 213 cs := &constraint{ 214 function: constraintOps[m[1]], 215 msg: constraintMsg[m[1]], 216 con: con, 217 orig: orig, 218 minorDirty: minorDirty, 219 patchDirty: patchDirty, 220 dirty: dirty, 221 } 222 return cs, nil 223} 224 225// Constraint functions 226func constraintNotEqual(v *Version, c *constraint) bool { 227 if c.dirty { 228 229 // If there is a pre-release on the version but the constraint isn't looking 230 // for them assume that pre-releases are not compatible. See issue 21 for 231 // more details. 232 if v.Prerelease() != "" && c.con.Prerelease() == "" { 233 return false 234 } 235 236 if c.con.Major() != v.Major() { 237 return true 238 } 239 if c.con.Minor() != v.Minor() && !c.minorDirty { 240 return true 241 } else if c.minorDirty { 242 return false 243 } 244 245 return false 246 } 247 248 return !v.Equal(c.con) 249} 250 251func constraintGreaterThan(v *Version, c *constraint) bool { 252 253 // If there is a pre-release on the version but the constraint isn't looking 254 // for them assume that pre-releases are not compatible. See issue 21 for 255 // more details. 256 if v.Prerelease() != "" && c.con.Prerelease() == "" { 257 return false 258 } 259 260 return v.Compare(c.con) == 1 261} 262 263func constraintLessThan(v *Version, c *constraint) bool { 264 // If there is a pre-release on the version but the constraint isn't looking 265 // for them assume that pre-releases are not compatible. See issue 21 for 266 // more details. 267 if v.Prerelease() != "" && c.con.Prerelease() == "" { 268 return false 269 } 270 271 if !c.dirty { 272 return v.Compare(c.con) < 0 273 } 274 275 if v.Major() > c.con.Major() { 276 return false 277 } else if v.Minor() > c.con.Minor() && !c.minorDirty { 278 return false 279 } 280 281 return true 282} 283 284func constraintGreaterThanEqual(v *Version, c *constraint) bool { 285 286 // If there is a pre-release on the version but the constraint isn't looking 287 // for them assume that pre-releases are not compatible. See issue 21 for 288 // more details. 289 if v.Prerelease() != "" && c.con.Prerelease() == "" { 290 return false 291 } 292 293 return v.Compare(c.con) >= 0 294} 295 296func constraintLessThanEqual(v *Version, c *constraint) bool { 297 // If there is a pre-release on the version but the constraint isn't looking 298 // for them assume that pre-releases are not compatible. See issue 21 for 299 // more details. 300 if v.Prerelease() != "" && c.con.Prerelease() == "" { 301 return false 302 } 303 304 if !c.dirty { 305 return v.Compare(c.con) <= 0 306 } 307 308 if v.Major() > c.con.Major() { 309 return false 310 } else if v.Minor() > c.con.Minor() && !c.minorDirty { 311 return false 312 } 313 314 return true 315} 316 317// ~*, ~>* --> >= 0.0.0 (any) 318// ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0, <3.0.0 319// ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0, <2.1.0 320// ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0, <1.3.0 321// ~1.2.3, ~>1.2.3 --> >=1.2.3, <1.3.0 322// ~1.2.0, ~>1.2.0 --> >=1.2.0, <1.3.0 323func constraintTilde(v *Version, c *constraint) bool { 324 // If there is a pre-release on the version but the constraint isn't looking 325 // for them assume that pre-releases are not compatible. See issue 21 for 326 // more details. 327 if v.Prerelease() != "" && c.con.Prerelease() == "" { 328 return false 329 } 330 331 if v.LessThan(c.con) { 332 return false 333 } 334 335 // ~0.0.0 is a special case where all constraints are accepted. It's 336 // equivalent to >= 0.0.0. 337 if c.con.Major() == 0 && c.con.Minor() == 0 && c.con.Patch() == 0 && 338 !c.minorDirty && !c.patchDirty { 339 return true 340 } 341 342 if v.Major() != c.con.Major() { 343 return false 344 } 345 346 if v.Minor() != c.con.Minor() && !c.minorDirty { 347 return false 348 } 349 350 return true 351} 352 353// When there is a .x (dirty) status it automatically opts in to ~. Otherwise 354// it's a straight = 355func constraintTildeOrEqual(v *Version, c *constraint) bool { 356 // If there is a pre-release on the version but the constraint isn't looking 357 // for them assume that pre-releases are not compatible. See issue 21 for 358 // more details. 359 if v.Prerelease() != "" && c.con.Prerelease() == "" { 360 return false 361 } 362 363 if c.dirty { 364 c.msg = constraintMsg["~"] 365 return constraintTilde(v, c) 366 } 367 368 return v.Equal(c.con) 369} 370 371// ^* --> (any) 372// ^2, ^2.x, ^2.x.x --> >=2.0.0, <3.0.0 373// ^2.0, ^2.0.x --> >=2.0.0, <3.0.0 374// ^1.2, ^1.2.x --> >=1.2.0, <2.0.0 375// ^1.2.3 --> >=1.2.3, <2.0.0 376// ^1.2.0 --> >=1.2.0, <2.0.0 377func constraintCaret(v *Version, c *constraint) bool { 378 // If there is a pre-release on the version but the constraint isn't looking 379 // for them assume that pre-releases are not compatible. See issue 21 for 380 // more details. 381 if v.Prerelease() != "" && c.con.Prerelease() == "" { 382 return false 383 } 384 385 if v.LessThan(c.con) { 386 return false 387 } 388 389 if v.Major() != c.con.Major() { 390 return false 391 } 392 393 return true 394} 395 396var constraintRangeRegex *regexp.Regexp 397 398const cvRegex string = `v?([0-9|x|X|\*]+)(\.[0-9|x|X|\*]+)?(\.[0-9|x|X|\*]+)?` + 399 `(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` + 400 `(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` 401 402func isX(x string) bool { 403 switch x { 404 case "x", "*", "X": 405 return true 406 default: 407 return false 408 } 409} 410 411func rewriteRange(i string) string { 412 m := constraintRangeRegex.FindAllStringSubmatch(i, -1) 413 if m == nil { 414 return i 415 } 416 o := i 417 for _, v := range m { 418 t := fmt.Sprintf(">= %s, <= %s", v[1], v[11]) 419 o = strings.Replace(o, v[0], t, 1) 420 } 421 422 return o 423} 424