podman
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 module36
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 {55if major == "" {56major = "v0"57}58segment := fmt.Sprintf("%s-%s", t.UTC().Format(PseudoVersionTimestampFormat), rev)59build := semver.Build(older)60older = semver.Canonical(older)61if older == "" {62return major + ".0.0-" + segment // form (1)63}64if semver.Prerelease(older) != "" {65return older + ".0." + segment + build // form (4), (5)66}67
68// Form (2), (3).69// Extract patch from vMAJOR.MINOR.PATCH70i := strings.LastIndex(older, ".") + 171v, patch := older[:i], older[i:]72
73// Reassemble.74return v + incDecimal(patch) + "-0." + segment + build75}
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 {80return 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.86digits := []byte(decimal)87i := len(digits) - 188for ; i >= 0 && digits[i] == '9'; i-- {89digits[i] = '0'90}91if i >= 0 {92digits[i]++93} else {94// digits is all zeros95digits[0] = '1'96digits = append(digits, '0')97}98return 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.105digits := []byte(decimal)106i := len(digits) - 1107for ; i >= 0 && digits[i] == '0'; i-- {108digits[i] = '9'109}110if i < 0 {111// decimal is all zeros112return ""113}114if i == 0 && digits[i] == '1' && len(digits) > 1 {115digits = digits[1:]116} else {117digits[i]--118}119return string(digits)120}
121
122// IsPseudoVersion reports whether v is a pseudo-version.
123func IsPseudoVersion(v string) bool {124return 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 {130return 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)138if err != nil {139return time.Time{}, err140}141t, err := time.Parse("20060102150405", timestamp)142if err != nil {143return time.Time{}, &InvalidVersionError{144Version: v,145Pseudo: true,146Err: fmt.Errorf("malformed time %q", timestamp),147}148}149return t, nil150}
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)156return157}
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) {165base, _, _, build, err := parsePseudoVersion(v)166if err != nil {167return "", err168}169
170switch pre := semver.Prerelease(base); pre {171case "":172// vX.0.0-yyyymmddhhmmss-abcdef123456 → ""173if build != "" {174// Pseudo-versions of the form vX.0.0-yyyymmddhhmmss-abcdef123456+incompatible175// are nonsensical: the "vX.0.0-" prefix implies that there is no parent tag,176// but the "+incompatible" suffix implies that the major version of177// 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.181return "", &InvalidVersionError{182Version: v,183Pseudo: true,184Err: fmt.Errorf("lacks base version, but has build metadata %q", build),185}186}187return "", nil188
189case "-0":190// vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456 → vX.Y.Z191// vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456+incompatible → vX.Y.Z+incompatible192base = strings.TrimSuffix(base, pre)193i := strings.LastIndexByte(base, '.')194if i < 0 {195panic("base from parsePseudoVersion missing patch number: " + base)196}197patch := decDecimal(base[i+1:])198if patch == "" {199// vX.0.0-0 is invalid, but has been observed in the wild in the index200// generated by requests to proxy.golang.org.201//202// NOTE(bcmills): I cannot find a historical bug that accounts for203// pseudo-versions of this form, nor have I seen such versions in any204// actual go.mod files. If we find actual examples of this form and a205// reasonable theory of how they came into existence, it seems fine to206// treat them as equivalent to vX.0.0 (especially since the invalid207// pseudo-versions have lower precedence than the real ones). For now, we208// reject them.209return "", &InvalidVersionError{210Version: v,211Pseudo: true,212Err: fmt.Errorf("version before %s would have negative patch number", base),213}214}215return base[:i+1] + patch + build, nil216
217default:218// vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456 → vX.Y.Z-pre219// vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456+incompatible → vX.Y.Z-pre+incompatible220if !strings.HasSuffix(base, ".0") {221panic(`base from parsePseudoVersion missing ".0" before date: ` + base)222}223return strings.TrimSuffix(base, ".0") + build, nil224}225}
226
227var errPseudoSyntax = errors.New("syntax error")228
229func parsePseudoVersion(v string) (base, timestamp, rev, build string, err error) {230if !IsPseudoVersion(v) {231return "", "", "", "", &InvalidVersionError{232Version: v,233Pseudo: true,234Err: errPseudoSyntax,235}236}237build = semver.Build(v)238v = strings.TrimSuffix(v, build)239j := strings.LastIndex(v, "-")240v, rev = v[:j], v[j+1:]241i := strings.LastIndex(v, "-")242if j := strings.LastIndex(v, "."); j > i {243base = v[:j] // "vX.Y.Z-pre.0" or "vX.Y.(Z+1)-0"244timestamp = v[j+1:]245} else {246base = v[:i] // "vX.0.0"247timestamp = v[i+1:]248}249return base, timestamp, rev, build, nil250}
251