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 modfetch 36 37 import ( 38 "errors" 39 "fmt" 40 "strings" 41 "time" 42 43 "cmd/go/internal/module" 44 "cmd/go/internal/semver" 45 "internal/lazyregexp" 46 ) 47 48 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-]+)*)?$`) 49 50 // PseudoVersion returns a pseudo-version for the given major version ("v1") 51 // preexisting older tagged version ("" or "v1.2.3" or "v1.2.3-pre"), revision time, 52 // and revision identifier (usually a 12-byte commit hash prefix). 53 func PseudoVersion(major, older string, t time.Time, rev string) string { 54 if major == "" { 55 major = "v0" 56 } 57 segment := fmt.Sprintf("%s-%s", t.UTC().Format("20060102150405"), rev) 58 build := semver.Build(older) 59 older = semver.Canonical(older) 60 if older == "" { 61 return major + ".0.0-" + segment // form (1) 62 } 63 if semver.Prerelease(older) != "" { 64 return older + ".0." + segment + build // form (4), (5) 65 } 66 67 // Form (2), (3). 68 // Extract patch from vMAJOR.MINOR.PATCH 69 i := strings.LastIndex(older, ".") + 1 70 v, patch := older[:i], older[i:] 71 72 // Reassemble. 73 return v + incDecimal(patch) + "-0." + segment + build 74 } 75 76 // incDecimal returns the decimal string incremented by 1. 77 func incDecimal(decimal string) string { 78 // Scan right to left turning 9s to 0s until you find a digit to increment. 79 digits := []byte(decimal) 80 i := len(digits) - 1 81 for ; i >= 0 && digits[i] == '9'; i-- { 82 digits[i] = '0' 83 } 84 if i >= 0 { 85 digits[i]++ 86 } else { 87 // digits is all zeros 88 digits[0] = '1' 89 digits = append(digits, '0') 90 } 91 return string(digits) 92 } 93 94 // decDecimal returns the decimal string decremented by 1, or the empty string 95 // if the decimal is all zeroes. 96 func decDecimal(decimal string) string { 97 // Scan right to left turning 0s to 9s until you find a digit to decrement. 98 digits := []byte(decimal) 99 i := len(digits) - 1 100 for ; i >= 0 && digits[i] == '0'; i-- { 101 digits[i] = '9' 102 } 103 if i < 0 { 104 // decimal is all zeros 105 return "" 106 } 107 if i == 0 && digits[i] == '1' && len(digits) > 1 { 108 digits = digits[1:] 109 } else { 110 digits[i]-- 111 } 112 return string(digits) 113 } 114 115 // IsPseudoVersion reports whether v is a pseudo-version. 116 func IsPseudoVersion(v string) bool { 117 return strings.Count(v, "-") >= 2 && semver.IsValid(v) && pseudoVersionRE.MatchString(v) 118 } 119 120 // PseudoVersionTime returns the time stamp of the pseudo-version v. 121 // It returns an error if v is not a pseudo-version or if the time stamp 122 // embedded in the pseudo-version is not a valid time. 123 func PseudoVersionTime(v string) (time.Time, error) { 124 _, timestamp, _, _, err := parsePseudoVersion(v) 125 if err != nil { 126 return time.Time{}, err 127 } 128 t, err := time.Parse("20060102150405", timestamp) 129 if err != nil { 130 return time.Time{}, &module.InvalidVersionError{ 131 Version: v, 132 Pseudo: true, 133 Err: fmt.Errorf("malformed time %q", timestamp), 134 } 135 } 136 return t, nil 137 } 138 139 // PseudoVersionRev returns the revision identifier of the pseudo-version v. 140 // It returns an error if v is not a pseudo-version. 141 func PseudoVersionRev(v string) (rev string, err error) { 142 _, _, rev, _, err = parsePseudoVersion(v) 143 return 144 } 145 146 // PseudoVersionBase returns the canonical parent version, if any, upon which 147 // the pseudo-version v is based. 148 // 149 // If v has no parent version (that is, if it is "vX.0.0-[…]"), 150 // PseudoVersionBase returns the empty string and a nil error. 151 func PseudoVersionBase(v string) (string, error) { 152 base, _, _, build, err := parsePseudoVersion(v) 153 if err != nil { 154 return "", err 155 } 156 157 switch pre := semver.Prerelease(base); pre { 158 case "": 159 // vX.0.0-yyyymmddhhmmss-abcdef123456 → "" 160 if build != "" { 161 // Pseudo-versions of the form vX.0.0-yyyymmddhhmmss-abcdef123456+incompatible 162 // are nonsensical: the "vX.0.0-" prefix implies that there is no parent tag, 163 // but the "+incompatible" suffix implies that the major version of 164 // the parent tag is not compatible with the module's import path. 165 // 166 // There are a few such entries in the index generated by proxy.golang.org, 167 // but we believe those entries were generated by the proxy itself. 168 return "", &module.InvalidVersionError{ 169 Version: v, 170 Pseudo: true, 171 Err: fmt.Errorf("lacks base version, but has build metadata %q", build), 172 } 173 } 174 return "", nil 175 176 case "-0": 177 // vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456 → vX.Y.Z 178 // vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456+incompatible → vX.Y.Z+incompatible 179 base = strings.TrimSuffix(base, pre) 180 i := strings.LastIndexByte(base, '.') 181 if i < 0 { 182 panic("base from parsePseudoVersion missing patch number: " + base) 183 } 184 patch := decDecimal(base[i+1:]) 185 if patch == "" { 186 // vX.0.0-0 is invalid, but has been observed in the wild in the index 187 // generated by requests to proxy.golang.org. 188 // 189 // NOTE(bcmills): I cannot find a historical bug that accounts for 190 // pseudo-versions of this form, nor have I seen such versions in any 191 // actual go.mod files. If we find actual examples of this form and a 192 // reasonable theory of how they came into existence, it seems fine to 193 // treat them as equivalent to vX.0.0 (especially since the invalid 194 // pseudo-versions have lower precedence than the real ones). For now, we 195 // reject them. 196 return "", &module.InvalidVersionError{ 197 Version: v, 198 Pseudo: true, 199 Err: fmt.Errorf("version before %s would have negative patch number", base), 200 } 201 } 202 return base[:i+1] + patch + build, nil 203 204 default: 205 // vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456 → vX.Y.Z-pre 206 // vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456+incompatible → vX.Y.Z-pre+incompatible 207 if !strings.HasSuffix(base, ".0") { 208 panic(`base from parsePseudoVersion missing ".0" before date: ` + base) 209 } 210 return strings.TrimSuffix(base, ".0") + build, nil 211 } 212 } 213 214 var errPseudoSyntax = errors.New("syntax error") 215 216 func parsePseudoVersion(v string) (base, timestamp, rev, build string, err error) { 217 if !IsPseudoVersion(v) { 218 return "", "", "", "", &module.InvalidVersionError{ 219 Version: v, 220 Pseudo: true, 221 Err: errPseudoSyntax, 222 } 223 } 224 build = semver.Build(v) 225 v = strings.TrimSuffix(v, build) 226 j := strings.LastIndex(v, "-") 227 v, rev = v[:j], v[j+1:] 228 i := strings.LastIndex(v, "-") 229 if j := strings.LastIndex(v, "."); j > i { 230 base = v[:j] // "vX.Y.Z-pre.0" or "vX.Y.(Z+1)-0" 231 timestamp = v[j+1:] 232 } else { 233 base = v[:i] // "vX.0.0" 234 timestamp = v[i+1:] 235 } 236 return base, timestamp, rev, build, nil 237 } 238