package version import ( "bytes" "fmt" "reflect" "regexp" "strconv" "strings" ) // The compiled regular expression used to test the validity of a version. var ( versionRegexp *regexp.Regexp semverRegexp *regexp.Regexp ) // The raw regular expression string used for testing the validity // of a version. const ( VersionRegexpRaw string = `(?:\w+\-)*[vV]?` + // Optional prefixes, will be ignored for parsing `([0-9]+(\.[0-9]+)*?)` + // ( MajorNum ( '.' MinorNums ) *? ) `(-` + // Followed by (optionally): ( '-' `([0-9]+[0-9A-Za-z\-~]*(\.[0-9A-Za-z\-~]+)*)` + // Either ( PreNum String ( '.' OtherString ) * ) `|` + `([-\.]?([A-Za-z\-~]+[0-9A-Za-z\-~]*(\.[0-9A-Za-z\-~]+)*)))?` + // Or ( ['-' '.' ] ? ( AlphaHyphenTilde String * ( '.' String ) * ))) ? `(\+([0-9A-Za-z\-~]+(\.[0-9A-Za-z\-~]+)*))?` + // and more Optionally: ( '+' String ( '.' String ) * ) `([\+\.\-~]g[0-9A-Fa-f]{10}$)?` + // Optionally a: ( Punct 'g' Sha ) `?` // SemverRegexpRaw requires a separator between version and prerelease SemverRegexpRaw string = `v?([0-9]+(\.[0-9]+)*?)` + `(-([0-9]+[0-9A-Za-z\-~]*(\.[0-9A-Za-z\-~]+)*)|(-([A-Za-z\-~]+[0-9A-Za-z\-~]*(\.[0-9A-Za-z\-~]+)*)))?` + `(\+([0-9A-Za-z\-~]+(\.[0-9A-Za-z\-~]+)*))?` + `?` ) // Version represents a single version. type Version struct { metadata string pre string segments []int64 si int original string } func init() { versionRegexp = regexp.MustCompile("^" + VersionRegexpRaw + "$") semverRegexp = regexp.MustCompile("^" + SemverRegexpRaw + "$") } // NewVersion parses the given version and returns a new // Version. func NewVersion(v string) (*Version, error) { return newVersion(v, versionRegexp) } // NewSemver parses the given version and returns a new // Version that adheres strictly to SemVer specs // https://semver.org/ func NewSemver(v string) (*Version, error) { return newVersion(v, semverRegexp) } func newVersion(v string, pattern *regexp.Regexp) (*Version, error) { matches := pattern.FindStringSubmatch(v) if matches == nil { return nil, fmt.Errorf("Malformed version: %s", v) } segmentsStr := strings.Split(matches[1], ".") segments := make([]int64, len(segmentsStr)) si := 0 for i, str := range segmentsStr { val, err := strconv.ParseInt(str, 10, 64) if err != nil { return nil, fmt.Errorf( "Error parsing version: %s", err) } segments[i] = int64(val) si++ } // Even though we could support more than three segments, if we // got less than three, pad it with 0s. This is to cover the basic // default usecase of semver, which is MAJOR.MINOR.PATCH at the minimum for i := len(segments); i < 3; i++ { segments = append(segments, 0) } pre := matches[7] if pre == "" { pre = matches[4] } return &Version{ metadata: matches[10], pre: pre, segments: segments, si: si, original: v, }, nil } // Must is a helper that wraps a call to a function returning (*Version, error) // and panics if error is non-nil. func Must(v *Version, err error) *Version { if err != nil { panic(err) } return v } // Compare compares this version to another version. This // returns -1, 0, or 1 if this version is smaller, equal, // or larger than the other version, respectively. // // If you want boolean results, use the LessThan, Equal, // GreaterThan, GreaterThanOrEqual or LessThanOrEqual methods. func (v *Version) Compare(other *Version) int { // A quick, efficient equality check if v.String() == other.String() { return 0 } segmentsSelf := v.Segments64() segmentsOther := other.Segments64() // If the segments are the same, we must compare on prerelease info if reflect.DeepEqual(segmentsSelf, segmentsOther) { preSelf := v.Prerelease() preOther := other.Prerelease() if preSelf == "" && preOther == "" { return 0 } if preSelf == "" { return 1 } if preOther == "" { return -1 } return comparePrereleases(preSelf, preOther) } // Get the highest specificity (hS), or if they're equal, just use segmentSelf length lenSelf := len(segmentsSelf) lenOther := len(segmentsOther) hS := lenSelf if lenSelf < lenOther { hS = lenOther } // Compare the segments // Because a constraint could have more/less specificity than the version it's // checking, we need to account for a lopsided or jagged comparison for i := 0; i < hS; i++ { if i > lenSelf-1 { // This means Self had the lower specificity // Check to see if the remaining segments in Other are all zeros if !allZero(segmentsOther[i:]) { // if not, it means that Other has to be greater than Self return -1 } break } else if i > lenOther-1 { // this means Other had the lower specificity // Check to see if the remaining segments in Self are all zeros - if !allZero(segmentsSelf[i:]) { // if not, it means that Self has to be greater than Other return 1 } break } lhs := segmentsSelf[i] rhs := segmentsOther[i] if lhs == rhs { continue } else if lhs < rhs { return -1 } // Otherwis, rhs was > lhs, they're not equal return 1 } // if we got this far, they're equal return 0 } func allZero(segs []int64) bool { for _, s := range segs { if s != 0 { return false } } return true } func comparePart(preSelf string, preOther string) int { if preSelf == preOther { return 0 } var selfInt int64 selfNumeric := true selfInt, err := strconv.ParseInt(preSelf, 10, 64) if err != nil { selfNumeric = false } var otherInt int64 otherNumeric := true otherInt, err = strconv.ParseInt(preOther, 10, 64) if err != nil { otherNumeric = false } // if a part is empty, we use the other to decide if preSelf == "" { if otherNumeric { return -1 } return 1 } if preOther == "" { if selfNumeric { return 1 } return -1 } if selfNumeric && !otherNumeric { return -1 } else if !selfNumeric && otherNumeric { return 1 } else if !selfNumeric && !otherNumeric && preSelf > preOther { return 1 } else if selfInt > otherInt { return 1 } return -1 } func comparePrereleases(v string, other string) int { // the same pre release! if v == other { return 0 } // split both pre releases for analyse their parts selfPreReleaseMeta := strings.Split(v, ".") otherPreReleaseMeta := strings.Split(other, ".") selfPreReleaseLen := len(selfPreReleaseMeta) otherPreReleaseLen := len(otherPreReleaseMeta) biggestLen := otherPreReleaseLen if selfPreReleaseLen > otherPreReleaseLen { biggestLen = selfPreReleaseLen } // loop for parts to find the first difference for i := 0; i < biggestLen; i++ { partSelfPre := "" if i < selfPreReleaseLen { partSelfPre = selfPreReleaseMeta[i] } partOtherPre := "" if i < otherPreReleaseLen { partOtherPre = otherPreReleaseMeta[i] } compare := comparePart(partSelfPre, partOtherPre) // if parts are equals, continue the loop if compare != 0 { return compare } } return 0 } // Core returns a new version constructed from only the MAJOR.MINOR.PATCH // segments of the version, without prerelease or metadata. func (v *Version) Core() *Version { segments := v.Segments64() segmentsOnly := fmt.Sprintf("%d.%d.%d", segments[0], segments[1], segments[2]) return Must(NewVersion(segmentsOnly)) } // Equal tests if two versions are equal. func (v *Version) Equal(o *Version) bool { if v == nil || o == nil { return v == o } return v.Compare(o) == 0 } // GreaterThan tests if this version is greater than another version. func (v *Version) GreaterThan(o *Version) bool { return v.Compare(o) > 0 } // GreaterThanOrEqual tests if this version is greater than or equal to another version. func (v *Version) GreaterThanOrEqual(o *Version) bool { return v.Compare(o) >= 0 } // LessThan tests if this version is less than another version. func (v *Version) LessThan(o *Version) bool { return v.Compare(o) < 0 } // LessThanOrEqual tests if this version is less than or equal to another version. func (v *Version) LessThanOrEqual(o *Version) bool { return v.Compare(o) <= 0 } // Metadata returns any metadata that was part of the version // string. // // Metadata is anything that comes after the "+" in the version. // For example, with "1.2.3+beta", the metadata is "beta". func (v *Version) Metadata() string { return v.metadata } // Prerelease returns any prerelease data that is part of the version, // or blank if there is no prerelease data. // // Prerelease information is anything that comes after the "-" in the // version (but before any metadata). For example, with "1.2.3-beta", // the prerelease information is "beta". func (v *Version) Prerelease() string { return v.pre } // Segments returns the numeric segments of the version as a slice of ints. // // This excludes any metadata or pre-release information. For example, // for a version "1.2.3-beta", segments will return a slice of // 1, 2, 3. func (v *Version) Segments() []int { segmentSlice := make([]int, len(v.segments)) for i, v := range v.segments { segmentSlice[i] = int(v) } return segmentSlice } // Segments64 returns the numeric segments of the version as a slice of int64s. // // This excludes any metadata or pre-release information. For example, // for a version "1.2.3-beta", segments will return a slice of // 1, 2, 3. func (v *Version) Segments64() []int64 { result := make([]int64, len(v.segments)) copy(result, v.segments) return result } // String returns the full version string included pre-release // and metadata information. // // This value is rebuilt according to the parsed segments and other // information. Therefore, ambiguities in the version string such as // prefixed zeroes (1.04.0 => 1.4.0), `v` prefix (v1.0.0 => 1.0.0), and // missing parts (1.0 => 1.0.0) will be made into a canonicalized form // as shown in the parenthesized examples. func (v *Version) String() string { var buf bytes.Buffer fmtParts := make([]string, len(v.segments)) for i, s := range v.segments { // We can ignore err here since we've pre-parsed the values in segments str := strconv.FormatInt(s, 10) fmtParts[i] = str } fmt.Fprint(&buf, strings.Join(fmtParts, ".")) if v.pre != "" { fmt.Fprintf(&buf, "-%s", v.pre) } if v.metadata != "" { fmt.Fprintf(&buf, "+%s", v.metadata) } return buf.String() } // Original returns the original parsed version as-is, including any // potential whitespace, `v` prefix, etc. func (v *Version) Original() string { return v.original } // RemoveMeta remove metadata // original parsed version data is not touched func (v *Version) RemoveMeta() { v.metadata = "" } // RemovePre remove pre-release data // original parsed version data is not touched func (v *Version) RemovePre() { v.pre = "" }