module: move pseudo-version functions into module

For golang/go#44969

Change-Id: Ie094d59140764b7f1cffb879d99a13da23a977da
Reviewed-on: https://go-review.googlesource.com/c/mod/+/304150
Trust: Jay Conrod <jayconrod@google.com>
Run-TryBot: Jay Conrod <jayconrod@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Bryan C. Mills <bcmills@google.com>
diff --git a/module/pseudo.go b/module/pseudo.go
new file mode 100644
index 0000000..f04ad37
--- /dev/null
+++ b/module/pseudo.go
@@ -0,0 +1,250 @@
+// 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 module
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+	"time"
+
+	"golang.org/x/mod/internal/lazyregexp"
+	"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{}, &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 "", &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 "", &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 "", "", "", "", &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
+}
diff --git a/module/pseudo_test.go b/module/pseudo_test.go
new file mode 100644
index 0000000..7c16c9c
--- /dev/null
+++ b/module/pseudo_test.go
@@ -0,0 +1,154 @@
+// 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.
+
+package module
+
+import (
+	"testing"
+	"time"
+)
+
+var pseudoTests = []struct {
+	major   string
+	older   string
+	version string
+}{
+	{"", "", "v0.0.0-20060102150405-hash"},
+	{"v0", "", "v0.0.0-20060102150405-hash"},
+	{"v1", "", "v1.0.0-20060102150405-hash"},
+	{"v2", "", "v2.0.0-20060102150405-hash"},
+	{"unused", "v0.0.0", "v0.0.1-0.20060102150405-hash"},
+	{"unused", "v1.2.3", "v1.2.4-0.20060102150405-hash"},
+	{"unused", "v1.2.99999999999999999", "v1.2.100000000000000000-0.20060102150405-hash"},
+	{"unused", "v1.2.3-pre", "v1.2.3-pre.0.20060102150405-hash"},
+	{"unused", "v1.3.0-pre", "v1.3.0-pre.0.20060102150405-hash"},
+	{"unused", "v0.0.0--", "v0.0.0--.0.20060102150405-hash"},
+	{"unused", "v1.0.0+metadata", "v1.0.1-0.20060102150405-hash+metadata"},
+	{"unused", "v2.0.0+incompatible", "v2.0.1-0.20060102150405-hash+incompatible"},
+	{"unused", "v2.3.0-pre+incompatible", "v2.3.0-pre.0.20060102150405-hash+incompatible"},
+}
+
+var pseudoTime = time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC)
+
+func TestPseudoVersion(t *testing.T) {
+	for _, tt := range pseudoTests {
+		v := PseudoVersion(tt.major, tt.older, pseudoTime, "hash")
+		if v != tt.version {
+			t.Errorf("PseudoVersion(%q, %q, ...) = %v, want %v", tt.major, tt.older, v, tt.version)
+		}
+	}
+}
+
+func TestIsPseudoVersion(t *testing.T) {
+	for _, tt := range pseudoTests {
+		if !IsPseudoVersion(tt.version) {
+			t.Errorf("IsPseudoVersion(%q) = false, want true", tt.version)
+		}
+		if IsPseudoVersion(tt.older) {
+			t.Errorf("IsPseudoVersion(%q) = true, want false", tt.older)
+		}
+	}
+}
+
+func TestPseudoVersionTime(t *testing.T) {
+	for _, tt := range pseudoTests {
+		tm, err := PseudoVersionTime(tt.version)
+		if tm != pseudoTime || err != nil {
+			t.Errorf("PseudoVersionTime(%q) = %v, %v, want %v, nil", tt.version, tm.Format(time.RFC3339), err, pseudoTime.Format(time.RFC3339))
+		}
+		tm, err = PseudoVersionTime(tt.older)
+		if tm != (time.Time{}) || err == nil {
+			t.Errorf("PseudoVersionTime(%q) = %v, %v, want %v, error", tt.older, tm.Format(time.RFC3339), err, time.Time{}.Format(time.RFC3339))
+		}
+	}
+}
+
+func TestInvalidPseudoVersionTime(t *testing.T) {
+	const v = "---"
+	if _, err := PseudoVersionTime(v); err == nil {
+		t.Error("expected error, got nil instead")
+	}
+}
+
+func TestPseudoVersionRev(t *testing.T) {
+	for _, tt := range pseudoTests {
+		rev, err := PseudoVersionRev(tt.version)
+		if rev != "hash" || err != nil {
+			t.Errorf("PseudoVersionRev(%q) = %q, %v, want %q, nil", tt.older, rev, err, "hash")
+		}
+		rev, err = PseudoVersionRev(tt.older)
+		if rev != "" || err == nil {
+			t.Errorf("PseudoVersionRev(%q) = %q, %v, want %q, error", tt.older, rev, err, "")
+		}
+	}
+}
+
+func TestPseudoVersionBase(t *testing.T) {
+	for _, tt := range pseudoTests {
+		base, err := PseudoVersionBase(tt.version)
+		if err != nil {
+			t.Errorf("PseudoVersionBase(%q): %v", tt.version, err)
+		} else if base != tt.older {
+			t.Errorf("PseudoVersionBase(%q) = %q; want %q", tt.version, base, tt.older)
+		}
+	}
+}
+
+func TestInvalidPseudoVersionBase(t *testing.T) {
+	for _, in := range []string{
+		"v0.0.0",
+		"v0.0.0-",                                 // malformed: empty prerelease
+		"v0.0.0-0.20060102150405-hash",            // Z+1 == 0
+		"v0.1.0-0.20060102150405-hash",            // Z+1 == 0
+		"v1.0.0-0.20060102150405-hash",            // Z+1 == 0
+		"v0.0.0-20060102150405-hash+incompatible", // "+incompatible without base version
+		"v0.0.0-20060102150405-hash+metadata",     // other metadata without base version
+	} {
+		base, err := PseudoVersionBase(in)
+		if err == nil || base != "" {
+			t.Errorf(`PseudoVersionBase(%q) = %q, %v; want "", error`, in, base, err)
+		}
+	}
+}
+
+func TestIncDecimal(t *testing.T) {
+	cases := []struct {
+		in, want string
+	}{
+		{"0", "1"},
+		{"1", "2"},
+		{"99", "100"},
+		{"100", "101"},
+		{"101", "102"},
+	}
+
+	for _, tc := range cases {
+		got := incDecimal(tc.in)
+		if got != tc.want {
+			t.Fatalf("incDecimal(%q) = %q; want %q", tc.in, tc.want, got)
+		}
+	}
+}
+
+func TestDecDecimal(t *testing.T) {
+	cases := []struct {
+		in, want string
+	}{
+		{"", ""},
+		{"0", ""},
+		{"00", ""},
+		{"1", "0"},
+		{"2", "1"},
+		{"99", "98"},
+		{"100", "99"},
+		{"101", "100"},
+	}
+
+	for _, tc := range cases {
+		got := decDecimal(tc.in)
+		if got != tc.want {
+			t.Fatalf("decDecimal(%q) = %q; want %q", tc.in, tc.want, got)
+		}
+	}
+}