// Copyright 2022 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package semver provides shared utilities for manipulating // Go semantic versions. package semver import ( "fmt" "regexp" "strings" "golang.org/x/mod/semver" ) // addSemverPrefix adds a 'v' prefix to s if it isn't already prefixed // with 'v' or 'go'. This allows us to easily test go-style SEMVER // strings against normal SEMVER strings. func addSemverPrefix(s string) string { if !strings.HasPrefix(s, "v") && !strings.HasPrefix(s, "go") { return "v" + s } return s } // removeSemverPrefix removes the 'v' or 'go' prefixes from go-style // SEMVER strings, for usage in the public vulnerability format. func removeSemverPrefix(s string) string { s = strings.TrimPrefix(s, "v") s = strings.TrimPrefix(s, "go") return s } // canonicalizeSemverPrefix turns a SEMVER string into the canonical // representation using the 'v' prefix, as used by the OSV format. // Input may be a bare SEMVER ("1.2.3"), Go prefixed SEMVER ("go1.2.3"), // or already canonical SEMVER ("v1.2.3"). func canonicalizeSemverPrefix(s string) string { return addSemverPrefix(removeSemverPrefix(s)) } // Less returns whether v1 < v2, where v1 and v2 are // semver versions with either a "v", "go" or no prefix. func Less(v1, v2 string) bool { return semver.Compare(canonicalizeSemverPrefix(v1), canonicalizeSemverPrefix(v2)) < 0 } // Valid returns whether v is valid semver, allowing // either a "v", "go" or no prefix. func Valid(v string) bool { return semver.IsValid(canonicalizeSemverPrefix(v)) } var ( // Regexp for matching go tags. The groups are: // 1 the major.minor version // 2 the patch version, or empty if none // 3 the entire prerelease, if present // 4 the prerelease type ("beta" or "rc") // 5 the prerelease number tagRegexp = regexp.MustCompile(`^go(\d+\.\d+)(\.\d+|)((beta|rc|-pre)(\d+))?$`) ) // This is a modified copy of pkgsite/internal/stdlib:VersionForTag. func GoTagToSemver(tag string) string { if tag == "" { return "" } tag = strings.Fields(tag)[0] // Special cases for go1. if tag == "go1" { return "v1.0.0" } if tag == "go1.0" { return "" } m := tagRegexp.FindStringSubmatch(tag) if m == nil { return "" } version := "v" + m[1] if m[2] != "" { version += m[2] } else { version += ".0" } if m[3] != "" { if !strings.HasPrefix(m[4], "-") { version += "-" } version += m[4] + "." + m[5] } return version } // This is a modified copy of pkgsite/internal/stlib:TagForVersion func SemverToGoTag(v string) string { // Special case: v1.0.0 => go1. if v == "v1.0.0" { return "go1" } goVersion := semver.Canonical(v) prerelease := semver.Prerelease(goVersion) versionWithoutPrerelease := strings.TrimSuffix(goVersion, prerelease) patch := strings.TrimPrefix(versionWithoutPrerelease, semver.MajorMinor(goVersion)+".") if patch == "0" && (semver.Compare(v, "v1.21.0") < 0 || prerelease != "") { // Starting with go1.21.0, the first patch version includes .0. // Prereleases do not include .0 (we don't do prereleases for other patch releases). versionWithoutPrerelease = strings.TrimSuffix(versionWithoutPrerelease, ".0") } goVersion = fmt.Sprintf("go%s", strings.TrimPrefix(versionWithoutPrerelease, "v")) if prerelease != "" { i := finalDigitsIndex(prerelease) if i >= 1 { // Remove the dot. prerelease = prerelease[:i-1] + prerelease[i:] } goVersion += prerelease } return goVersion } // finalDigitsIndex returns the index of the first digit in the sequence of digits ending s. // If s doesn't end in digits, it returns -1. func finalDigitsIndex(s string) int { // Assume ASCII (since the semver package does anyway). var i int for i = len(s) - 1; i >= 0; i-- { if s[i] < '0' || s[i] > '9' { break } } if i == len(s)-1 { return -1 } return i + 1 }