...

Source file src/golang.org/x/mod/module/pseudo.go

Documentation: golang.org/x/mod/module

     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  

View as plain text