package semver import ( "errors" "fmt" "strconv" "strings" ) const ( numbers string = "0123456789" alphas = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-" alphanum = alphas + numbers ) // SpecVersion is the latest fully supported spec version of semver var SpecVersion = Version{ Major: 2, Minor: 0, Patch: 0, } // Version represents a semver compatible version type Version struct { Major uint64 Minor uint64 Patch uint64 Pre []PRVersion Build []string //No Precendence } // Version to string func (v Version) String() string { b := make([]byte, 0, 5) b = strconv.AppendUint(b, v.Major, 10) b = append(b, '.') b = strconv.AppendUint(b, v.Minor, 10) b = append(b, '.') b = strconv.AppendUint(b, v.Patch, 10) if len(v.Pre) > 0 { b = append(b, '-') b = append(b, v.Pre[0].String()...) for _, pre := range v.Pre[1:] { b = append(b, '.') b = append(b, pre.String()...) } } if len(v.Build) > 0 { b = append(b, '+') b = append(b, v.Build[0]...) for _, build := range v.Build[1:] { b = append(b, '.') b = append(b, build...) } } return string(b) } // Equals checks if v is equal to o. func (v Version) Equals(o Version) bool { return (v.Compare(o) == 0) } // EQ checks if v is equal to o. func (v Version) EQ(o Version) bool { return (v.Compare(o) == 0) } // NE checks if v is not equal to o. func (v Version) NE(o Version) bool { return (v.Compare(o) != 0) } // GT checks if v is greater than o. func (v Version) GT(o Version) bool { return (v.Compare(o) == 1) } // GTE checks if v is greater than or equal to o. func (v Version) GTE(o Version) bool { return (v.Compare(o) >= 0) } // GE checks if v is greater than or equal to o. func (v Version) GE(o Version) bool { return (v.Compare(o) >= 0) } // LT checks if v is less than o. func (v Version) LT(o Version) bool { return (v.Compare(o) == -1) } // LTE checks if v is less than or equal to o. func (v Version) LTE(o Version) bool { return (v.Compare(o) <= 0) } // LE checks if v is less than or equal to o. func (v Version) LE(o Version) bool { return (v.Compare(o) <= 0) } // Compare compares Versions v to o: // -1 == v is less than o // 0 == v is equal to o // 1 == v is greater than o func (v Version) Compare(o Version) int { if v.Major != o.Major { if v.Major > o.Major { return 1 } return -1 } if v.Minor != o.Minor { if v.Minor > o.Minor { return 1 } return -1 } if v.Patch != o.Patch { if v.Patch > o.Patch { return 1 } return -1 } // Quick comparison if a version has no prerelease versions if len(v.Pre) == 0 && len(o.Pre) == 0 { return 0 } else if len(v.Pre) == 0 && len(o.Pre) > 0 { return 1 } else if len(v.Pre) > 0 && len(o.Pre) == 0 { return -1 } i := 0 for ; i < len(v.Pre) && i < len(o.Pre); i++ { if comp := v.Pre[i].Compare(o.Pre[i]); comp == 0 { continue } else if comp == 1 { return 1 } else { return -1 } } // If all pr versions are the equal but one has further prversion, this one greater if i == len(v.Pre) && i == len(o.Pre) { return 0 } else if i == len(v.Pre) && i < len(o.Pre) { return -1 } else { return 1 } } // Validate validates v and returns error in case func (v Version) Validate() error { // Major, Minor, Patch already validated using uint64 for _, pre := range v.Pre { if !pre.IsNum { //Numeric prerelease versions already uint64 if len(pre.VersionStr) == 0 { return fmt.Errorf("Prerelease can not be empty %q", pre.VersionStr) } if !containsOnly(pre.VersionStr, alphanum) { return fmt.Errorf("Invalid character(s) found in prerelease %q", pre.VersionStr) } } } for _, build := range v.Build { if len(build) == 0 { return fmt.Errorf("Build meta data can not be empty %q", build) } if !containsOnly(build, alphanum) { return fmt.Errorf("Invalid character(s) found in build meta data %q", build) } } return nil } // New is an alias for Parse and returns a pointer, parses version string and returns a validated Version or error func New(s string) (vp *Version, err error) { v, err := Parse(s) vp = &v return } // Make is an alias for Parse, parses version string and returns a validated Version or error func Make(s string) (Version, error) { return Parse(s) } // ParseTolerant allows for certain version specifications that do not strictly adhere to semver // specs to be parsed by this library. It does so by normalizing versions before passing them to // Parse(). It currently trims spaces, removes a "v" prefix, and adds a 0 patch number to versions // with only major and minor components specified func ParseTolerant(s string) (Version, error) { s = strings.TrimSpace(s) s = strings.TrimPrefix(s, "v") // Split into major.minor.(patch+pr+meta) parts := strings.SplitN(s, ".", 3) if len(parts) < 3 { if strings.ContainsAny(parts[len(parts)-1], "+-") { return Version{}, errors.New("Short version cannot contain PreRelease/Build meta data") } for len(parts) < 3 { parts = append(parts, "0") } s = strings.Join(parts, ".") } return Parse(s) } // Parse parses version string and returns a validated Version or error func Parse(s string) (Version, error) { if len(s) == 0 { return Version{}, errors.New("Version string empty") } // Split into major.minor.(patch+pr+meta) parts := strings.SplitN(s, ".", 3) if len(parts) != 3 { return Version{}, errors.New("No Major.Minor.Patch elements found") } // Major if !containsOnly(parts[0], numbers) { return Version{}, fmt.Errorf("Invalid character(s) found in major number %q", parts[0]) } if hasLeadingZeroes(parts[0]) { return Version{}, fmt.Errorf("Major number must not contain leading zeroes %q", parts[0]) } major, err := strconv.ParseUint(parts[0], 10, 64) if err != nil { return Version{}, err } // Minor if !containsOnly(parts[1], numbers) { return Version{}, fmt.Errorf("Invalid character(s) found in minor number %q", parts[1]) } if hasLeadingZeroes(parts[1]) { return Version{}, fmt.Errorf("Minor number must not contain leading zeroes %q", parts[1]) } minor, err := strconv.ParseUint(parts[1], 10, 64) if err != nil { return Version{}, err } v := Version{} v.Major = major v.Minor = minor var build, prerelease []string patchStr := parts[2] if buildIndex := strings.IndexRune(patchStr, '+'); buildIndex != -1 { build = strings.Split(patchStr[buildIndex+1:], ".") patchStr = patchStr[:buildIndex] } if preIndex := strings.IndexRune(patchStr, '-'); preIndex != -1 { prerelease = strings.Split(patchStr[preIndex+1:], ".") patchStr = patchStr[:preIndex] } if !containsOnly(patchStr, numbers) { return Version{}, fmt.Errorf("Invalid character(s) found in patch number %q", patchStr) } if hasLeadingZeroes(patchStr) { return Version{}, fmt.Errorf("Patch number must not contain leading zeroes %q", patchStr) } patch, err := strconv.ParseUint(patchStr, 10, 64) if err != nil { return Version{}, err } v.Patch = patch // Prerelease for _, prstr := range prerelease { parsedPR, err := NewPRVersion(prstr) if err != nil { return Version{}, err } v.Pre = append(v.Pre, parsedPR) } // Build meta data for _, str := range build { if len(str) == 0 { return Version{}, errors.New("Build meta data is empty") } if !containsOnly(str, alphanum) { return Version{}, fmt.Errorf("Invalid character(s) found in build meta data %q", str) } v.Build = append(v.Build, str) } return v, nil } // MustParse is like Parse but panics if the version cannot be parsed. func MustParse(s string) Version { v, err := Parse(s) if err != nil { panic(`semver: Parse(` + s + `): ` + err.Error()) } return v } // PRVersion represents a PreRelease Version type PRVersion struct { VersionStr string VersionNum uint64 IsNum bool } // NewPRVersion creates a new valid prerelease version func NewPRVersion(s string) (PRVersion, error) { if len(s) == 0 { return PRVersion{}, errors.New("Prerelease is empty") } v := PRVersion{} if containsOnly(s, numbers) { if hasLeadingZeroes(s) { return PRVersion{}, fmt.Errorf("Numeric PreRelease version must not contain leading zeroes %q", s) } num, err := strconv.ParseUint(s, 10, 64) // Might never be hit, but just in case if err != nil { return PRVersion{}, err } v.VersionNum = num v.IsNum = true } else if containsOnly(s, alphanum) { v.VersionStr = s v.IsNum = false } else { return PRVersion{}, fmt.Errorf("Invalid character(s) found in prerelease %q", s) } return v, nil } // IsNumeric checks if prerelease-version is numeric func (v PRVersion) IsNumeric() bool { return v.IsNum } // Compare compares two PreRelease Versions v and o: // -1 == v is less than o // 0 == v is equal to o // 1 == v is greater than o func (v PRVersion) Compare(o PRVersion) int { if v.IsNum && !o.IsNum { return -1 } else if !v.IsNum && o.IsNum { return 1 } else if v.IsNum && o.IsNum { if v.VersionNum == o.VersionNum { return 0 } else if v.VersionNum > o.VersionNum { return 1 } else { return -1 } } else { // both are Alphas if v.VersionStr == o.VersionStr { return 0 } else if v.VersionStr > o.VersionStr { return 1 } else { return -1 } } } // PreRelease version to string func (v PRVersion) String() string { if v.IsNum { return strconv.FormatUint(v.VersionNum, 10) } return v.VersionStr } func containsOnly(s string, set string) bool { return strings.IndexFunc(s, func(r rune) bool { return !strings.ContainsRune(set, r) }) == -1 } func hasLeadingZeroes(s string) bool { return len(s) > 1 && s[0] == '0' } // NewBuildVersion creates a new valid build version func NewBuildVersion(s string) (string, error) { if len(s) == 0 { return "", errors.New("Buildversion is empty") } if !containsOnly(s, alphanum) { return "", fmt.Errorf("Invalid character(s) found in build meta data %q", s) } return s, nil }