1// Copyright 2018 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// Pseudo-versions 6// 7// Code authors are expected to tag the revisions they want users to use, 8// including prereleases. However, not all authors tag versions at all, 9// and not all commits a user might want to try will have tags. 10// A pseudo-version is a version with a special form that allows us to 11// address an untagged commit and order that version with respect to 12// other versions we might encounter. 13// 14// A pseudo-version takes one of the general forms: 15// 16// (1) vX.0.0-yyyymmddhhmmss-abcdef123456 17// (2) vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456 18// (3) vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456+incompatible 19// (4) vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456 20// (5) vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456+incompatible 21// 22// If there is no recently tagged version with the right major version vX, 23// then form (1) is used, creating a space of pseudo-versions at the bottom 24// of the vX version range, less than any tagged version, including the unlikely v0.0.0. 25// 26// If the most recent tagged version before the target commit is vX.Y.Z or vX.Y.Z+incompatible, 27// then the pseudo-version uses form (2) or (3), making it a prerelease for the next 28// possible semantic version after vX.Y.Z. The leading 0 segment in the prerelease string 29// ensures that the pseudo-version compares less than possible future explicit prereleases 30// like vX.Y.(Z+1)-rc1 or vX.Y.(Z+1)-1. 31// 32// If the most recent tagged version before the target commit is vX.Y.Z-pre or vX.Y.Z-pre+incompatible, 33// then the pseudo-version uses form (4) or (5), making it a slightly later prerelease. 34 35package module 36 37import ( 38 "errors" 39 "fmt" 40 "strings" 41 "time" 42 43 "golang.org/x/mod/internal/lazyregexp" 44 "golang.org/x/mod/semver" 45) 46 47var pseudoVersionRE = lazyregexp.New(`^v[0-9]+\.(0\.0-|\d+\.\d+-([^+]*\.)?0\.)\d{14}-[A-Za-z0-9]+(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$`) 48 49const PseudoVersionTimestampFormat = "20060102150405" 50 51// PseudoVersion returns a pseudo-version for the given major version ("v1") 52// preexisting older tagged version ("" or "v1.2.3" or "v1.2.3-pre"), revision time, 53// and revision identifier (usually a 12-byte commit hash prefix). 54func PseudoVersion(major, older string, t time.Time, rev string) string { 55 if major == "" { 56 major = "v0" 57 } 58 segment := fmt.Sprintf("%s-%s", t.UTC().Format(PseudoVersionTimestampFormat), rev) 59 build := semver.Build(older) 60 older = semver.Canonical(older) 61 if older == "" { 62 return major + ".0.0-" + segment // form (1) 63 } 64 if semver.Prerelease(older) != "" { 65 return older + ".0." + segment + build // form (4), (5) 66 } 67 68 // Form (2), (3). 69 // Extract patch from vMAJOR.MINOR.PATCH 70 i := strings.LastIndex(older, ".") + 1 71 v, patch := older[:i], older[i:] 72 73 // Reassemble. 74 return v + incDecimal(patch) + "-0." + segment + build 75} 76 77// ZeroPseudoVersion returns a pseudo-version with a zero timestamp and 78// revision, which may be used as a placeholder. 79func ZeroPseudoVersion(major string) string { 80 return PseudoVersion(major, "", time.Time{}, "000000000000") 81} 82 83// incDecimal returns the decimal string incremented by 1. 84func incDecimal(decimal string) string { 85 // Scan right to left turning 9s to 0s until you find a digit to increment. 86 digits := []byte(decimal) 87 i := len(digits) - 1 88 for ; i >= 0 && digits[i] == '9'; i-- { 89 digits[i] = '0' 90 } 91 if i >= 0 { 92 digits[i]++ 93 } else { 94 // digits is all zeros 95 digits[0] = '1' 96 digits = append(digits, '0') 97 } 98 return string(digits) 99} 100 101// decDecimal returns the decimal string decremented by 1, or the empty string 102// if the decimal is all zeroes. 103func decDecimal(decimal string) string { 104 // Scan right to left turning 0s to 9s until you find a digit to decrement. 105 digits := []byte(decimal) 106 i := len(digits) - 1 107 for ; i >= 0 && digits[i] == '0'; i-- { 108 digits[i] = '9' 109 } 110 if i < 0 { 111 // decimal is all zeros 112 return "" 113 } 114 if i == 0 && digits[i] == '1' && len(digits) > 1 { 115 digits = digits[1:] 116 } else { 117 digits[i]-- 118 } 119 return string(digits) 120} 121 122// IsPseudoVersion reports whether v is a pseudo-version. 123func IsPseudoVersion(v string) bool { 124 return strings.Count(v, "-") >= 2 && semver.IsValid(v) && pseudoVersionRE.MatchString(v) 125} 126 127// IsZeroPseudoVersion returns whether v is a pseudo-version with a zero base, 128// timestamp, and revision, as returned by [ZeroPseudoVersion]. 129func IsZeroPseudoVersion(v string) bool { 130 return v == ZeroPseudoVersion(semver.Major(v)) 131} 132 133// PseudoVersionTime returns the time stamp of the pseudo-version v. 134// It returns an error if v is not a pseudo-version or if the time stamp 135// embedded in the pseudo-version is not a valid time. 136func PseudoVersionTime(v string) (time.Time, error) { 137 _, timestamp, _, _, err := parsePseudoVersion(v) 138 if err != nil { 139 return time.Time{}, err 140 } 141 t, err := time.Parse("20060102150405", timestamp) 142 if err != nil { 143 return time.Time{}, &InvalidVersionError{ 144 Version: v, 145 Pseudo: true, 146 Err: fmt.Errorf("malformed time %q", timestamp), 147 } 148 } 149 return t, nil 150} 151 152// PseudoVersionRev returns the revision identifier of the pseudo-version v. 153// It returns an error if v is not a pseudo-version. 154func PseudoVersionRev(v string) (rev string, err error) { 155 _, _, rev, _, err = parsePseudoVersion(v) 156 return 157} 158 159// PseudoVersionBase returns the canonical parent version, if any, upon which 160// the pseudo-version v is based. 161// 162// If v has no parent version (that is, if it is "vX.0.0-[…]"), 163// PseudoVersionBase returns the empty string and a nil error. 164func PseudoVersionBase(v string) (string, error) { 165 base, _, _, build, err := parsePseudoVersion(v) 166 if err != nil { 167 return "", err 168 } 169 170 switch pre := semver.Prerelease(base); pre { 171 case "": 172 // vX.0.0-yyyymmddhhmmss-abcdef123456 → "" 173 if build != "" { 174 // Pseudo-versions of the form vX.0.0-yyyymmddhhmmss-abcdef123456+incompatible 175 // are nonsensical: the "vX.0.0-" prefix implies that there is no parent tag, 176 // but the "+incompatible" suffix implies that the major version of 177 // the parent tag is not compatible with the module's import path. 178 // 179 // There are a few such entries in the index generated by proxy.golang.org, 180 // but we believe those entries were generated by the proxy itself. 181 return "", &InvalidVersionError{ 182 Version: v, 183 Pseudo: true, 184 Err: fmt.Errorf("lacks base version, but has build metadata %q", build), 185 } 186 } 187 return "", nil 188 189 case "-0": 190 // vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456 → vX.Y.Z 191 // vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456+incompatible → vX.Y.Z+incompatible 192 base = strings.TrimSuffix(base, pre) 193 i := strings.LastIndexByte(base, '.') 194 if i < 0 { 195 panic("base from parsePseudoVersion missing patch number: " + base) 196 } 197 patch := decDecimal(base[i+1:]) 198 if patch == "" { 199 // vX.0.0-0 is invalid, but has been observed in the wild in the index 200 // generated by requests to proxy.golang.org. 201 // 202 // NOTE(bcmills): I cannot find a historical bug that accounts for 203 // pseudo-versions of this form, nor have I seen such versions in any 204 // actual go.mod files. If we find actual examples of this form and a 205 // reasonable theory of how they came into existence, it seems fine to 206 // treat them as equivalent to vX.0.0 (especially since the invalid 207 // pseudo-versions have lower precedence than the real ones). For now, we 208 // reject them. 209 return "", &InvalidVersionError{ 210 Version: v, 211 Pseudo: true, 212 Err: fmt.Errorf("version before %s would have negative patch number", base), 213 } 214 } 215 return base[:i+1] + patch + build, nil 216 217 default: 218 // vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456 → vX.Y.Z-pre 219 // vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456+incompatible → vX.Y.Z-pre+incompatible 220 if !strings.HasSuffix(base, ".0") { 221 panic(`base from parsePseudoVersion missing ".0" before date: ` + base) 222 } 223 return strings.TrimSuffix(base, ".0") + build, nil 224 } 225} 226 227var errPseudoSyntax = errors.New("syntax error") 228 229func parsePseudoVersion(v string) (base, timestamp, rev, build string, err error) { 230 if !IsPseudoVersion(v) { 231 return "", "", "", "", &InvalidVersionError{ 232 Version: v, 233 Pseudo: true, 234 Err: errPseudoSyntax, 235 } 236 } 237 build = semver.Build(v) 238 v = strings.TrimSuffix(v, build) 239 j := strings.LastIndex(v, "-") 240 v, rev = v[:j], v[j+1:] 241 i := strings.LastIndex(v, "-") 242 if j := strings.LastIndex(v, "."); j > i { 243 base = v[:j] // "vX.Y.Z-pre.0" or "vX.Y.(Z+1)-0" 244 timestamp = v[j+1:] 245 } else { 246 base = v[:i] // "vX.0.0" 247 timestamp = v[i+1:] 248 } 249 return base, timestamp, rev, build, nil 250} 251