semver.go (3935B)
1 // Copyright 2022 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package semver provides shared utilities for manipulating 6 // Go semantic versions. 7 package semver 8 9 import ( 10 "fmt" 11 "regexp" 12 "strings" 13 14 "golang.org/x/mod/semver" 15 ) 16 17 // addSemverPrefix adds a 'v' prefix to s if it isn't already prefixed 18 // with 'v' or 'go'. This allows us to easily test go-style SEMVER 19 // strings against normal SEMVER strings. 20 func addSemverPrefix(s string) string { 21 if !strings.HasPrefix(s, "v") && !strings.HasPrefix(s, "go") { 22 return "v" + s 23 } 24 return s 25 } 26 27 // removeSemverPrefix removes the 'v' or 'go' prefixes from go-style 28 // SEMVER strings, for usage in the public vulnerability format. 29 func removeSemverPrefix(s string) string { 30 s = strings.TrimPrefix(s, "v") 31 s = strings.TrimPrefix(s, "go") 32 return s 33 } 34 35 // canonicalizeSemverPrefix turns a SEMVER string into the canonical 36 // representation using the 'v' prefix, as used by the OSV format. 37 // Input may be a bare SEMVER ("1.2.3"), Go prefixed SEMVER ("go1.2.3"), 38 // or already canonical SEMVER ("v1.2.3"). 39 func canonicalizeSemverPrefix(s string) string { 40 return addSemverPrefix(removeSemverPrefix(s)) 41 } 42 43 // Less returns whether v1 < v2, where v1 and v2 are 44 // semver versions with either a "v", "go" or no prefix. 45 func Less(v1, v2 string) bool { 46 return semver.Compare(canonicalizeSemverPrefix(v1), canonicalizeSemverPrefix(v2)) < 0 47 } 48 49 // Valid returns whether v is valid semver, allowing 50 // either a "v", "go" or no prefix. 51 func Valid(v string) bool { 52 return semver.IsValid(canonicalizeSemverPrefix(v)) 53 } 54 55 var ( 56 // Regexp for matching go tags. The groups are: 57 // 1 the major.minor version 58 // 2 the patch version, or empty if none 59 // 3 the entire prerelease, if present 60 // 4 the prerelease type ("beta" or "rc") 61 // 5 the prerelease number 62 tagRegexp = regexp.MustCompile(`^go(\d+\.\d+)(\.\d+|)((beta|rc|-pre)(\d+))?$`) 63 ) 64 65 // This is a modified copy of pkgsite/internal/stdlib:VersionForTag. 66 func GoTagToSemver(tag string) string { 67 if tag == "" { 68 return "" 69 } 70 71 tag = strings.Fields(tag)[0] 72 // Special cases for go1. 73 if tag == "go1" { 74 return "v1.0.0" 75 } 76 if tag == "go1.0" { 77 return "" 78 } 79 m := tagRegexp.FindStringSubmatch(tag) 80 if m == nil { 81 return "" 82 } 83 version := "v" + m[1] 84 if m[2] != "" { 85 version += m[2] 86 } else { 87 version += ".0" 88 } 89 if m[3] != "" { 90 if !strings.HasPrefix(m[4], "-") { 91 version += "-" 92 } 93 version += m[4] + "." + m[5] 94 } 95 return version 96 } 97 98 // This is a modified copy of pkgsite/internal/stlib:TagForVersion 99 func SemverToGoTag(v string) string { 100 // Special case: v1.0.0 => go1. 101 if v == "v1.0.0" { 102 return "go1" 103 } 104 105 goVersion := semver.Canonical(v) 106 prerelease := semver.Prerelease(goVersion) 107 versionWithoutPrerelease := strings.TrimSuffix(goVersion, prerelease) 108 patch := strings.TrimPrefix(versionWithoutPrerelease, semver.MajorMinor(goVersion)+".") 109 if patch == "0" && (semver.Compare(v, "v1.21.0") < 0 || prerelease != "") { 110 // Starting with go1.21.0, the first patch version includes .0. 111 // Prereleases do not include .0 (we don't do prereleases for other patch releases). 112 versionWithoutPrerelease = strings.TrimSuffix(versionWithoutPrerelease, ".0") 113 } 114 goVersion = fmt.Sprintf("go%s", strings.TrimPrefix(versionWithoutPrerelease, "v")) 115 if prerelease != "" { 116 i := finalDigitsIndex(prerelease) 117 if i >= 1 { 118 // Remove the dot. 119 prerelease = prerelease[:i-1] + prerelease[i:] 120 } 121 goVersion += prerelease 122 } 123 return goVersion 124 } 125 126 // finalDigitsIndex returns the index of the first digit in the sequence of digits ending s. 127 // If s doesn't end in digits, it returns -1. 128 func finalDigitsIndex(s string) int { 129 // Assume ASCII (since the semver package does anyway). 130 var i int 131 for i = len(s) - 1; i >= 0; i-- { 132 if s[i] < '0' || s[i] > '9' { 133 break 134 } 135 } 136 if i == len(s)-1 { 137 return -1 138 } 139 return i + 1 140 }