1package semver
2
3import (
4	"errors"
5	"fmt"
6	"strconv"
7	"strings"
8)
9
10const (
11	numbers  string = "0123456789"
12	alphas          = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-"
13	alphanum        = alphas + numbers
14)
15
16// SpecVersion is the latest fully supported spec version of semver
17var SpecVersion = Version{
18	Major: 2,
19	Minor: 0,
20	Patch: 0,
21}
22
23// Version represents a semver compatible version
24type Version struct {
25	Major uint64
26	Minor uint64
27	Patch uint64
28	Pre   []PRVersion
29	Build []string //No Precendence
30}
31
32// Version to string
33func (v Version) String() string {
34	b := make([]byte, 0, 5)
35	b = strconv.AppendUint(b, v.Major, 10)
36	b = append(b, '.')
37	b = strconv.AppendUint(b, v.Minor, 10)
38	b = append(b, '.')
39	b = strconv.AppendUint(b, v.Patch, 10)
40
41	if len(v.Pre) > 0 {
42		b = append(b, '-')
43		b = append(b, v.Pre[0].String()...)
44
45		for _, pre := range v.Pre[1:] {
46			b = append(b, '.')
47			b = append(b, pre.String()...)
48		}
49	}
50
51	if len(v.Build) > 0 {
52		b = append(b, '+')
53		b = append(b, v.Build[0]...)
54
55		for _, build := range v.Build[1:] {
56			b = append(b, '.')
57			b = append(b, build...)
58		}
59	}
60
61	return string(b)
62}
63
64// Equals checks if v is equal to o.
65func (v Version) Equals(o Version) bool {
66	return (v.Compare(o) == 0)
67}
68
69// EQ checks if v is equal to o.
70func (v Version) EQ(o Version) bool {
71	return (v.Compare(o) == 0)
72}
73
74// NE checks if v is not equal to o.
75func (v Version) NE(o Version) bool {
76	return (v.Compare(o) != 0)
77}
78
79// GT checks if v is greater than o.
80func (v Version) GT(o Version) bool {
81	return (v.Compare(o) == 1)
82}
83
84// GTE checks if v is greater than or equal to o.
85func (v Version) GTE(o Version) bool {
86	return (v.Compare(o) >= 0)
87}
88
89// GE checks if v is greater than or equal to o.
90func (v Version) GE(o Version) bool {
91	return (v.Compare(o) >= 0)
92}
93
94// LT checks if v is less than o.
95func (v Version) LT(o Version) bool {
96	return (v.Compare(o) == -1)
97}
98
99// LTE checks if v is less than or equal to o.
100func (v Version) LTE(o Version) bool {
101	return (v.Compare(o) <= 0)
102}
103
104// LE checks if v is less than or equal to o.
105func (v Version) LE(o Version) bool {
106	return (v.Compare(o) <= 0)
107}
108
109// Compare compares Versions v to o:
110// -1 == v is less than o
111// 0 == v is equal to o
112// 1 == v is greater than o
113func (v Version) Compare(o Version) int {
114	if v.Major != o.Major {
115		if v.Major > o.Major {
116			return 1
117		}
118		return -1
119	}
120	if v.Minor != o.Minor {
121		if v.Minor > o.Minor {
122			return 1
123		}
124		return -1
125	}
126	if v.Patch != o.Patch {
127		if v.Patch > o.Patch {
128			return 1
129		}
130		return -1
131	}
132
133	// Quick comparison if a version has no prerelease versions
134	if len(v.Pre) == 0 && len(o.Pre) == 0 {
135		return 0
136	} else if len(v.Pre) == 0 && len(o.Pre) > 0 {
137		return 1
138	} else if len(v.Pre) > 0 && len(o.Pre) == 0 {
139		return -1
140	}
141
142	i := 0
143	for ; i < len(v.Pre) && i < len(o.Pre); i++ {
144		if comp := v.Pre[i].Compare(o.Pre[i]); comp == 0 {
145			continue
146		} else if comp == 1 {
147			return 1
148		} else {
149			return -1
150		}
151	}
152
153	// If all pr versions are the equal but one has further prversion, this one greater
154	if i == len(v.Pre) && i == len(o.Pre) {
155		return 0
156	} else if i == len(v.Pre) && i < len(o.Pre) {
157		return -1
158	} else {
159		return 1
160	}
161
162}
163
164// Validate validates v and returns error in case
165func (v Version) Validate() error {
166	// Major, Minor, Patch already validated using uint64
167
168	for _, pre := range v.Pre {
169		if !pre.IsNum { //Numeric prerelease versions already uint64
170			if len(pre.VersionStr) == 0 {
171				return fmt.Errorf("Prerelease can not be empty %q", pre.VersionStr)
172			}
173			if !containsOnly(pre.VersionStr, alphanum) {
174				return fmt.Errorf("Invalid character(s) found in prerelease %q", pre.VersionStr)
175			}
176		}
177	}
178
179	for _, build := range v.Build {
180		if len(build) == 0 {
181			return fmt.Errorf("Build meta data can not be empty %q", build)
182		}
183		if !containsOnly(build, alphanum) {
184			return fmt.Errorf("Invalid character(s) found in build meta data %q", build)
185		}
186	}
187
188	return nil
189}
190
191// New is an alias for Parse and returns a pointer, parses version string and returns a validated Version or error
192func New(s string) (vp *Version, err error) {
193	v, err := Parse(s)
194	vp = &v
195	return
196}
197
198// Make is an alias for Parse, parses version string and returns a validated Version or error
199func Make(s string) (Version, error) {
200	return Parse(s)
201}
202
203// ParseTolerant allows for certain version specifications that do not strictly adhere to semver
204// specs to be parsed by this library. It does so by normalizing versions before passing them to
205// Parse(). It currently trims spaces, removes a "v" prefix, and adds a 0 patch number to versions
206// with only major and minor components specified
207func ParseTolerant(s string) (Version, error) {
208	s = strings.TrimSpace(s)
209	s = strings.TrimPrefix(s, "v")
210
211	// Split into major.minor.(patch+pr+meta)
212	parts := strings.SplitN(s, ".", 3)
213	if len(parts) < 3 {
214		if strings.ContainsAny(parts[len(parts)-1], "+-") {
215			return Version{}, errors.New("Short version cannot contain PreRelease/Build meta data")
216		}
217		for len(parts) < 3 {
218			parts = append(parts, "0")
219		}
220		s = strings.Join(parts, ".")
221	}
222
223	return Parse(s)
224}
225
226// Parse parses version string and returns a validated Version or error
227func Parse(s string) (Version, error) {
228	if len(s) == 0 {
229		return Version{}, errors.New("Version string empty")
230	}
231
232	// Split into major.minor.(patch+pr+meta)
233	parts := strings.SplitN(s, ".", 3)
234	if len(parts) != 3 {
235		return Version{}, errors.New("No Major.Minor.Patch elements found")
236	}
237
238	// Major
239	if !containsOnly(parts[0], numbers) {
240		return Version{}, fmt.Errorf("Invalid character(s) found in major number %q", parts[0])
241	}
242	if hasLeadingZeroes(parts[0]) {
243		return Version{}, fmt.Errorf("Major number must not contain leading zeroes %q", parts[0])
244	}
245	major, err := strconv.ParseUint(parts[0], 10, 64)
246	if err != nil {
247		return Version{}, err
248	}
249
250	// Minor
251	if !containsOnly(parts[1], numbers) {
252		return Version{}, fmt.Errorf("Invalid character(s) found in minor number %q", parts[1])
253	}
254	if hasLeadingZeroes(parts[1]) {
255		return Version{}, fmt.Errorf("Minor number must not contain leading zeroes %q", parts[1])
256	}
257	minor, err := strconv.ParseUint(parts[1], 10, 64)
258	if err != nil {
259		return Version{}, err
260	}
261
262	v := Version{}
263	v.Major = major
264	v.Minor = minor
265
266	var build, prerelease []string
267	patchStr := parts[2]
268
269	if buildIndex := strings.IndexRune(patchStr, '+'); buildIndex != -1 {
270		build = strings.Split(patchStr[buildIndex+1:], ".")
271		patchStr = patchStr[:buildIndex]
272	}
273
274	if preIndex := strings.IndexRune(patchStr, '-'); preIndex != -1 {
275		prerelease = strings.Split(patchStr[preIndex+1:], ".")
276		patchStr = patchStr[:preIndex]
277	}
278
279	if !containsOnly(patchStr, numbers) {
280		return Version{}, fmt.Errorf("Invalid character(s) found in patch number %q", patchStr)
281	}
282	if hasLeadingZeroes(patchStr) {
283		return Version{}, fmt.Errorf("Patch number must not contain leading zeroes %q", patchStr)
284	}
285	patch, err := strconv.ParseUint(patchStr, 10, 64)
286	if err != nil {
287		return Version{}, err
288	}
289
290	v.Patch = patch
291
292	// Prerelease
293	for _, prstr := range prerelease {
294		parsedPR, err := NewPRVersion(prstr)
295		if err != nil {
296			return Version{}, err
297		}
298		v.Pre = append(v.Pre, parsedPR)
299	}
300
301	// Build meta data
302	for _, str := range build {
303		if len(str) == 0 {
304			return Version{}, errors.New("Build meta data is empty")
305		}
306		if !containsOnly(str, alphanum) {
307			return Version{}, fmt.Errorf("Invalid character(s) found in build meta data %q", str)
308		}
309		v.Build = append(v.Build, str)
310	}
311
312	return v, nil
313}
314
315// MustParse is like Parse but panics if the version cannot be parsed.
316func MustParse(s string) Version {
317	v, err := Parse(s)
318	if err != nil {
319		panic(`semver: Parse(` + s + `): ` + err.Error())
320	}
321	return v
322}
323
324// PRVersion represents a PreRelease Version
325type PRVersion struct {
326	VersionStr string
327	VersionNum uint64
328	IsNum      bool
329}
330
331// NewPRVersion creates a new valid prerelease version
332func NewPRVersion(s string) (PRVersion, error) {
333	if len(s) == 0 {
334		return PRVersion{}, errors.New("Prerelease is empty")
335	}
336	v := PRVersion{}
337	if containsOnly(s, numbers) {
338		if hasLeadingZeroes(s) {
339			return PRVersion{}, fmt.Errorf("Numeric PreRelease version must not contain leading zeroes %q", s)
340		}
341		num, err := strconv.ParseUint(s, 10, 64)
342
343		// Might never be hit, but just in case
344		if err != nil {
345			return PRVersion{}, err
346		}
347		v.VersionNum = num
348		v.IsNum = true
349	} else if containsOnly(s, alphanum) {
350		v.VersionStr = s
351		v.IsNum = false
352	} else {
353		return PRVersion{}, fmt.Errorf("Invalid character(s) found in prerelease %q", s)
354	}
355	return v, nil
356}
357
358// IsNumeric checks if prerelease-version is numeric
359func (v PRVersion) IsNumeric() bool {
360	return v.IsNum
361}
362
363// Compare compares two PreRelease Versions v and o:
364// -1 == v is less than o
365// 0 == v is equal to o
366// 1 == v is greater than o
367func (v PRVersion) Compare(o PRVersion) int {
368	if v.IsNum && !o.IsNum {
369		return -1
370	} else if !v.IsNum && o.IsNum {
371		return 1
372	} else if v.IsNum && o.IsNum {
373		if v.VersionNum == o.VersionNum {
374			return 0
375		} else if v.VersionNum > o.VersionNum {
376			return 1
377		} else {
378			return -1
379		}
380	} else { // both are Alphas
381		if v.VersionStr == o.VersionStr {
382			return 0
383		} else if v.VersionStr > o.VersionStr {
384			return 1
385		} else {
386			return -1
387		}
388	}
389}
390
391// PreRelease version to string
392func (v PRVersion) String() string {
393	if v.IsNum {
394		return strconv.FormatUint(v.VersionNum, 10)
395	}
396	return v.VersionStr
397}
398
399func containsOnly(s string, set string) bool {
400	return strings.IndexFunc(s, func(r rune) bool {
401		return !strings.ContainsRune(set, r)
402	}) == -1
403}
404
405func hasLeadingZeroes(s string) bool {
406	return len(s) > 1 && s[0] == '0'
407}
408
409// NewBuildVersion creates a new valid build version
410func NewBuildVersion(s string) (string, error) {
411	if len(s) == 0 {
412		return "", errors.New("Buildversion is empty")
413	}
414	if !containsOnly(s, alphanum) {
415		return "", fmt.Errorf("Invalid character(s) found in build meta data %q", s)
416	}
417	return s, nil
418}
419