aboutsummaryrefslogtreecommitdiff
path: root/src/cmd/go/internal/modfetch/pseudo.go
blob: 93eb0fad961b6663dd2c21246ccfc0fe8d19dcfe (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
// Copyright 2018 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.

// Pseudo-versions
//
// Code authors are expected to tag the revisions they want users to use,
// including prereleases. However, not all authors tag versions at all,
// and not all commits a user might want to try will have tags.
// A pseudo-version is a version with a special form that allows us to
// address an untagged commit and order that version with respect to
// other versions we might encounter.
//
// A pseudo-version takes one of the general forms:
//
//	(1) vX.0.0-yyyymmddhhmmss-abcdef123456
//	(2) vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456
//	(3) vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456+incompatible
//	(4) vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456
//	(5) vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456+incompatible
//
// If there is no recently tagged version with the right major version vX,
// then form (1) is used, creating a space of pseudo-versions at the bottom
// of the vX version range, less than any tagged version, including the unlikely v0.0.0.
//
// If the most recent tagged version before the target commit is vX.Y.Z or vX.Y.Z+incompatible,
// then the pseudo-version uses form (2) or (3), making it a prerelease for the next
// possible semantic version after vX.Y.Z. The leading 0 segment in the prerelease string
// ensures that the pseudo-version compares less than possible future explicit prereleases
// like vX.Y.(Z+1)-rc1 or vX.Y.(Z+1)-1.
//
// If the most recent tagged version before the target commit is vX.Y.Z-pre or vX.Y.Z-pre+incompatible,
// then the pseudo-version uses form (4) or (5), making it a slightly later prerelease.

package modfetch

import (
	"errors"
	"fmt"
	"strings"
	"time"

	"internal/lazyregexp"

	"golang.org/x/mod/module"
	"golang.org/x/mod/semver"
)

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-]+)*)?$`)

const pseudoVersionTimestampFormat = "20060102150405"

// PseudoVersion returns a pseudo-version for the given major version ("v1")
// preexisting older tagged version ("" or "v1.2.3" or "v1.2.3-pre"), revision time,
// and revision identifier (usually a 12-byte commit hash prefix).
func PseudoVersion(major, older string, t time.Time, rev string) string {
	if major == "" {
		major = "v0"
	}
	segment := fmt.Sprintf("%s-%s", t.UTC().Format(pseudoVersionTimestampFormat), rev)
	build := semver.Build(older)
	older = semver.Canonical(older)
	if older == "" {
		return major + ".0.0-" + segment // form (1)
	}
	if semver.Prerelease(older) != "" {
		return older + ".0." + segment + build // form (4), (5)
	}

	// Form (2), (3).
	// Extract patch from vMAJOR.MINOR.PATCH
	i := strings.LastIndex(older, ".") + 1
	v, patch := older[:i], older[i:]

	// Reassemble.
	return v + incDecimal(patch) + "-0." + segment + build
}

// ZeroPseudoVersion returns a pseudo-version with a zero timestamp and
// revision, which may be used as a placeholder.
func ZeroPseudoVersion(major string) string {
	return PseudoVersion(major, "", time.Time{}, "000000000000")
}

// incDecimal returns the decimal string incremented by 1.
func incDecimal(decimal string) string {
	// Scan right to left turning 9s to 0s until you find a digit to increment.
	digits := []byte(decimal)
	i := len(digits) - 1
	for ; i >= 0 && digits[i] == '9'; i-- {
		digits[i] = '0'
	}
	if i >= 0 {
		digits[i]++
	} else {
		// digits is all zeros
		digits[0] = '1'
		digits = append(digits, '0')
	}
	return string(digits)
}

// decDecimal returns the decimal string decremented by 1, or the empty string
// if the decimal is all zeroes.
func decDecimal(decimal string) string {
	// Scan right to left turning 0s to 9s until you find a digit to decrement.
	digits := []byte(decimal)
	i := len(digits) - 1
	for ; i >= 0 && digits[i] == '0'; i-- {
		digits[i] = '9'
	}
	if i < 0 {
		// decimal is all zeros
		return ""
	}
	if i == 0 && digits[i] == '1' && len(digits) > 1 {
		digits = digits[1:]
	} else {
		digits[i]--
	}
	return string(digits)
}

// IsPseudoVersion reports whether v is a pseudo-version.
func IsPseudoVersion(v string) bool {
	return strings.Count(v, "-") >= 2 && semver.IsValid(v) && pseudoVersionRE.MatchString(v)
}

// IsZeroPseudoVersion returns whether v is a pseudo-version with a zero base,
// timestamp, and revision, as returned by ZeroPseudoVersion.
func IsZeroPseudoVersion(v string) bool {
	return v == ZeroPseudoVersion(semver.Major(v))
}

// PseudoVersionTime returns the time stamp of the pseudo-version v.
// It returns an error if v is not a pseudo-version or if the time stamp
// embedded in the pseudo-version is not a valid time.
func PseudoVersionTime(v string) (time.Time, error) {
	_, timestamp, _, _, err := parsePseudoVersion(v)
	if err != nil {
		return time.Time{}, err
	}
	t, err := time.Parse("20060102150405", timestamp)
	if err != nil {
		return time.Time{}, &module.InvalidVersionError{
			Version: v,
			Pseudo:  true,
			Err:     fmt.Errorf("malformed time %q", timestamp),
		}
	}
	return t, nil
}

// PseudoVersionRev returns the revision identifier of the pseudo-version v.
// It returns an error if v is not a pseudo-version.
func PseudoVersionRev(v string) (rev string, err error) {
	_, _, rev, _, err = parsePseudoVersion(v)
	return
}

// PseudoVersionBase returns the canonical parent version, if any, upon which
// the pseudo-version v is based.
//
// If v has no parent version (that is, if it is "vX.0.0-[…]"),
// PseudoVersionBase returns the empty string and a nil error.
func PseudoVersionBase(v string) (string, error) {
	base, _, _, build, err := parsePseudoVersion(v)
	if err != nil {
		return "", err
	}

	switch pre := semver.Prerelease(base); pre {
	case "":
		// vX.0.0-yyyymmddhhmmss-abcdef123456 → ""
		if build != "" {
			// Pseudo-versions of the form vX.0.0-yyyymmddhhmmss-abcdef123456+incompatible
			// are nonsensical: the "vX.0.0-" prefix implies that there is no parent tag,
			// but the "+incompatible" suffix implies that the major version of
			// the parent tag is not compatible with the module's import path.
			//
			// There are a few such entries in the index generated by proxy.golang.org,
			// but we believe those entries were generated by the proxy itself.
			return "", &module.InvalidVersionError{
				Version: v,
				Pseudo:  true,
				Err:     fmt.Errorf("lacks base version, but has build metadata %q", build),
			}
		}
		return "", nil

	case "-0":
		// vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456 → vX.Y.Z
		// vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456+incompatible → vX.Y.Z+incompatible
		base = strings.TrimSuffix(base, pre)
		i := strings.LastIndexByte(base, '.')
		if i < 0 {
			panic("base from parsePseudoVersion missing patch number: " + base)
		}
		patch := decDecimal(base[i+1:])
		if patch == "" {
			// vX.0.0-0 is invalid, but has been observed in the wild in the index
			// generated by requests to proxy.golang.org.
			//
			// NOTE(bcmills): I cannot find a historical bug that accounts for
			// pseudo-versions of this form, nor have I seen such versions in any
			// actual go.mod files. If we find actual examples of this form and a
			// reasonable theory of how they came into existence, it seems fine to
			// treat them as equivalent to vX.0.0 (especially since the invalid
			// pseudo-versions have lower precedence than the real ones). For now, we
			// reject them.
			return "", &module.InvalidVersionError{
				Version: v,
				Pseudo:  true,
				Err:     fmt.Errorf("version before %s would have negative patch number", base),
			}
		}
		return base[:i+1] + patch + build, nil

	default:
		// vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456 → vX.Y.Z-pre
		// vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456+incompatible → vX.Y.Z-pre+incompatible
		if !strings.HasSuffix(base, ".0") {
			panic(`base from parsePseudoVersion missing ".0" before date: ` + base)
		}
		return strings.TrimSuffix(base, ".0") + build, nil
	}
}

var errPseudoSyntax = errors.New("syntax error")

func parsePseudoVersion(v string) (base, timestamp, rev, build string, err error) {
	if !IsPseudoVersion(v) {
		return "", "", "", "", &module.InvalidVersionError{
			Version: v,
			Pseudo:  true,
			Err:     errPseudoSyntax,
		}
	}
	build = semver.Build(v)
	v = strings.TrimSuffix(v, build)
	j := strings.LastIndex(v, "-")
	v, rev = v[:j], v[j+1:]
	i := strings.LastIndex(v, "-")
	if j := strings.LastIndex(v, "."); j > i {
		base = v[:j] // "vX.Y.Z-pre.0" or "vX.Y.(Z+1)-0"
		timestamp = v[j+1:]
	} else {
		base = v[:i] // "vX.0.0"
		timestamp = v[i+1:]
	}
	return base, timestamp, rev, build, nil
}