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 35 package module 36 37 import ( 38 "errors" 39 "fmt" 40 "strings" 41 "time" 42 43 "golang.org/x/mod/internal/lazyregexp" 44 "golang.org/x/mod/semver" 45 ) 46 47 var 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 49 const 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). 54 func 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. 79 func ZeroPseudoVersion(major string) string { 80 return PseudoVersion(major, "", time.Time{}, "000000000000") 81 } 82 83 // incDecimal returns the decimal string incremented by 1. 84 func 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. 103 func 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. 123 func 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]. 129 func 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. 136 func 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. 154 func 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. 164 func 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 227 var errPseudoSyntax = errors.New("syntax error") 228 229 func 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