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