mirror of
https://github.com/strongdm/comply
synced 2024-11-14 11:54:53 +00:00
376 lines
8.6 KiB
Go
376 lines
8.6 KiB
Go
|
package semver
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
numbers string = "0123456789"
|
||
|
alphas = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-"
|
||
|
alphanum = alphas + numbers
|
||
|
dot = "."
|
||
|
hyphen = "-"
|
||
|
plus = "+"
|
||
|
)
|
||
|
|
||
|
// Latest fully supported spec version
|
||
|
var SPEC_VERSION = Version{
|
||
|
Major: 2,
|
||
|
Minor: 0,
|
||
|
Patch: 0,
|
||
|
}
|
||
|
|
||
|
type Version struct {
|
||
|
Major uint64
|
||
|
Minor uint64
|
||
|
Patch uint64
|
||
|
Pre []*PRVersion
|
||
|
Build []string //No Precendence
|
||
|
}
|
||
|
|
||
|
// Version to string
|
||
|
func (v Version) String() string {
|
||
|
versionArray := []string{
|
||
|
strconv.FormatUint(v.Major, 10),
|
||
|
dot,
|
||
|
strconv.FormatUint(v.Minor, 10),
|
||
|
dot,
|
||
|
strconv.FormatUint(v.Patch, 10),
|
||
|
}
|
||
|
if len(v.Pre) > 0 {
|
||
|
versionArray = append(versionArray, hyphen)
|
||
|
for i, pre := range v.Pre {
|
||
|
if i > 0 {
|
||
|
versionArray = append(versionArray, dot)
|
||
|
}
|
||
|
versionArray = append(versionArray, pre.String())
|
||
|
}
|
||
|
}
|
||
|
if len(v.Build) > 0 {
|
||
|
versionArray = append(versionArray, plus, strings.Join(v.Build, dot))
|
||
|
}
|
||
|
return strings.Join(versionArray, "")
|
||
|
}
|
||
|
|
||
|
// Checks if v is greater than o.
|
||
|
func (v Version) GT(o *Version) bool {
|
||
|
return (v.Compare(o) == 1)
|
||
|
}
|
||
|
|
||
|
// Checks if v is greater than or equal to o.
|
||
|
func (v Version) GTE(o *Version) bool {
|
||
|
return (v.Compare(o) >= 0)
|
||
|
}
|
||
|
|
||
|
// Checks if v is less than o.
|
||
|
func (v Version) LT(o *Version) bool {
|
||
|
return (v.Compare(o) == -1)
|
||
|
}
|
||
|
|
||
|
// Checks if v is less than or equal to o.
|
||
|
func (v Version) LTE(o *Version) bool {
|
||
|
return (v.Compare(o) <= 0)
|
||
|
}
|
||
|
|
||
|
// 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
|
||
|
} else {
|
||
|
return -1
|
||
|
}
|
||
|
}
|
||
|
if v.Minor != o.Minor {
|
||
|
if v.Minor > o.Minor {
|
||
|
return 1
|
||
|
} else {
|
||
|
return -1
|
||
|
}
|
||
|
}
|
||
|
if v.Patch != o.Patch {
|
||
|
if v.Patch > o.Patch {
|
||
|
return 1
|
||
|
} else {
|
||
|
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
|
||
|
} else {
|
||
|
|
||
|
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
|
||
|
}
|
||
|
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Validates v and returns error in case
|
||
|
func (v Version) Validate() error {
|
||
|
// Major, Minor, Patch already validated using uint64
|
||
|
|
||
|
if len(v.Pre) > 0 {
|
||
|
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)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if len(v.Build) > 0 {
|
||
|
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
|
||
|
}
|
||
|
|
||
|
// Alias for Parse, parses version string and returns a validated Version or error
|
||
|
func New(s string) (*Version, error) {
|
||
|
return Parse(s)
|
||
|
}
|
||
|
|
||
|
// Parses version string and returns a validated Version or error
|
||
|
func Parse(s string) (*Version, error) {
|
||
|
if len(s) == 0 {
|
||
|
return nil, errors.New("Version string empty")
|
||
|
}
|
||
|
|
||
|
// Split into major.minor.(patch+pr+meta)
|
||
|
parts := strings.SplitN(s, ".", 3)
|
||
|
if len(parts) != 3 {
|
||
|
return nil, errors.New("No Major.Minor.Patch elements found")
|
||
|
}
|
||
|
|
||
|
// Major
|
||
|
if !containsOnly(parts[0], numbers) {
|
||
|
return nil, fmt.Errorf("Invalid character(s) found in major number %q", parts[0])
|
||
|
}
|
||
|
if hasLeadingZeroes(parts[0]) {
|
||
|
return nil, fmt.Errorf("Major number must not contain leading zeroes %q", parts[0])
|
||
|
}
|
||
|
major, err := strconv.ParseUint(parts[0], 10, 64)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// Minor
|
||
|
if !containsOnly(parts[1], numbers) {
|
||
|
return nil, fmt.Errorf("Invalid character(s) found in minor number %q", parts[1])
|
||
|
}
|
||
|
if hasLeadingZeroes(parts[1]) {
|
||
|
return nil, fmt.Errorf("Minor number must not contain leading zeroes %q", parts[1])
|
||
|
}
|
||
|
minor, err := strconv.ParseUint(parts[1], 10, 64)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
preIndex := strings.Index(parts[2], "-")
|
||
|
buildIndex := strings.Index(parts[2], "+")
|
||
|
|
||
|
// Determine last index of patch version (first of pre or build versions)
|
||
|
var subVersionIndex int
|
||
|
if preIndex != -1 && buildIndex == -1 {
|
||
|
subVersionIndex = preIndex
|
||
|
} else if preIndex == -1 && buildIndex != -1 {
|
||
|
subVersionIndex = buildIndex
|
||
|
} else if preIndex == -1 && buildIndex == -1 {
|
||
|
subVersionIndex = len(parts[2])
|
||
|
} else {
|
||
|
// if there is no actual prversion but a hyphen inside the build meta data
|
||
|
if buildIndex < preIndex {
|
||
|
subVersionIndex = buildIndex
|
||
|
preIndex = -1 // Build meta data before preIndex found implicates there are no prerelease versions
|
||
|
} else {
|
||
|
subVersionIndex = preIndex
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if !containsOnly(parts[2][:subVersionIndex], numbers) {
|
||
|
return nil, fmt.Errorf("Invalid character(s) found in patch number %q", parts[2][:subVersionIndex])
|
||
|
}
|
||
|
if hasLeadingZeroes(parts[2][:subVersionIndex]) {
|
||
|
return nil, fmt.Errorf("Patch number must not contain leading zeroes %q", parts[2][:subVersionIndex])
|
||
|
}
|
||
|
patch, err := strconv.ParseUint(parts[2][:subVersionIndex], 10, 64)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
v := &Version{}
|
||
|
v.Major = major
|
||
|
v.Minor = minor
|
||
|
v.Patch = patch
|
||
|
|
||
|
// There are PreRelease versions
|
||
|
if preIndex != -1 {
|
||
|
var preRels string
|
||
|
if buildIndex != -1 {
|
||
|
preRels = parts[2][subVersionIndex+1 : buildIndex]
|
||
|
} else {
|
||
|
preRels = parts[2][subVersionIndex+1:]
|
||
|
}
|
||
|
prparts := strings.Split(preRels, ".")
|
||
|
for _, prstr := range prparts {
|
||
|
parsedPR, err := NewPRVersion(prstr)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
v.Pre = append(v.Pre, parsedPR)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// There is build meta data
|
||
|
if buildIndex != -1 {
|
||
|
buildStr := parts[2][buildIndex+1:]
|
||
|
buildParts := strings.Split(buildStr, ".")
|
||
|
for _, str := range buildParts {
|
||
|
if len(str) == 0 {
|
||
|
return nil, errors.New("Build meta data is empty")
|
||
|
}
|
||
|
if !containsOnly(str, alphanum) {
|
||
|
return nil, fmt.Errorf("Invalid character(s) found in build meta data %q", str)
|
||
|
}
|
||
|
v.Build = append(v.Build, str)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return v, nil
|
||
|
}
|
||
|
|
||
|
// PreRelease Version
|
||
|
type PRVersion struct {
|
||
|
VersionStr string
|
||
|
VersionNum uint64
|
||
|
IsNum bool
|
||
|
}
|
||
|
|
||
|
// Creates a new valid prerelease version
|
||
|
func NewPRVersion(s string) (*PRVersion, error) {
|
||
|
if len(s) == 0 {
|
||
|
return nil, errors.New("Prerelease is empty")
|
||
|
}
|
||
|
v := &PRVersion{}
|
||
|
if containsOnly(s, numbers) {
|
||
|
if hasLeadingZeroes(s) {
|
||
|
return nil, 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 nil, err
|
||
|
}
|
||
|
v.VersionNum = num
|
||
|
v.IsNum = true
|
||
|
} else if containsOnly(s, alphanum) {
|
||
|
v.VersionStr = s
|
||
|
v.IsNum = false
|
||
|
} else {
|
||
|
return nil, fmt.Errorf("Invalid character(s) found in prerelease %q", s)
|
||
|
}
|
||
|
return v, nil
|
||
|
}
|
||
|
|
||
|
// Is pre release version numeric?
|
||
|
func (v PRVersion) IsNumeric() bool {
|
||
|
return v.IsNum
|
||
|
}
|
||
|
|
||
|
// Compares PreRelease Versions v to 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'
|
||
|
}
|
||
|
|
||
|
// 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
|
||
|
}
|