whatcanGOwrong

This commit is contained in:
2024-09-19 21:38:24 -04:00
commit d0ae4d841d
17908 changed files with 4096831 additions and 0 deletions
@@ -0,0 +1,841 @@
// 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 defines the module.Version type along with support code.
//
// The [module.Version] type is a simple Path, Version pair:
//
// type Version struct {
// Path string
// Version string
// }
//
// There are no restrictions imposed directly by use of this structure,
// but additional checking functions, most notably [Check], verify that
// a particular path, version pair is valid.
//
// # Escaped Paths
//
// Module paths appear as substrings of file system paths
// (in the download cache) and of web server URLs in the proxy protocol.
// In general we cannot rely on file systems to be case-sensitive,
// nor can we rely on web servers, since they read from file systems.
// That is, we cannot rely on the file system to keep rsc.io/QUOTE
// and rsc.io/quote separate. Windows and macOS don't.
// Instead, we must never require two different casings of a file path.
// Because we want the download cache to match the proxy protocol,
// and because we want the proxy protocol to be possible to serve
// from a tree of static files (which might be stored on a case-insensitive
// file system), the proxy protocol must never require two different casings
// of a URL path either.
//
// One possibility would be to make the escaped form be the lowercase
// hexadecimal encoding of the actual path bytes. This would avoid ever
// needing different casings of a file path, but it would be fairly illegible
// to most programmers when those paths appeared in the file system
// (including in file paths in compiler errors and stack traces)
// in web server logs, and so on. Instead, we want a safe escaped form that
// leaves most paths unaltered.
//
// The safe escaped form is to replace every uppercase letter
// with an exclamation mark followed by the letter's lowercase equivalent.
//
// For example,
//
// github.com/Azure/azure-sdk-for-go -> github.com/!azure/azure-sdk-for-go.
// github.com/GoogleCloudPlatform/cloudsql-proxy -> github.com/!google!cloud!platform/cloudsql-proxy
// github.com/Sirupsen/logrus -> github.com/!sirupsen/logrus.
//
// Import paths that avoid upper-case letters are left unchanged.
// Note that because import paths are ASCII-only and avoid various
// problematic punctuation (like : < and >), the escaped form is also ASCII-only
// and avoids the same problematic punctuation.
//
// Import paths have never allowed exclamation marks, so there is no
// need to define how to escape a literal !.
//
// # Unicode Restrictions
//
// Today, paths are disallowed from using Unicode.
//
// Although paths are currently disallowed from using Unicode,
// we would like at some point to allow Unicode letters as well, to assume that
// file systems and URLs are Unicode-safe (storing UTF-8), and apply
// the !-for-uppercase convention for escaping them in the file system.
// But there are at least two subtle considerations.
//
// First, note that not all case-fold equivalent distinct runes
// form an upper/lower pair.
// For example, U+004B ('K'), U+006B ('k'), and U+212A ('' for Kelvin)
// are three distinct runes that case-fold to each other.
// When we do add Unicode letters, we must not assume that upper/lower
// are the only case-equivalent pairs.
// Perhaps the Kelvin symbol would be disallowed entirely, for example.
// Or perhaps it would escape as "!!k", or perhaps as "(212A)".
//
// Second, it would be nice to allow Unicode marks as well as letters,
// but marks include combining marks, and then we must deal not
// only with case folding but also normalization: both U+00E9 ('é')
// and U+0065 U+0301 ('e' followed by combining acute accent)
// look the same on the page and are treated by some file systems
// as the same path. If we do allow Unicode marks in paths, there
// must be some kind of normalization to allow only one canonical
// encoding of any character used in an import path.
package module
// IMPORTANT NOTE
//
// This file essentially defines the set of valid import paths for the go command.
// There are many subtle considerations, including Unicode ambiguity,
// security, network, and file system representations.
//
// This file also defines the set of valid module path and version combinations,
// another topic with many subtle considerations.
//
// Changes to the semantics in this file require approval from rsc.
import (
"errors"
"fmt"
"path"
"sort"
"strings"
"unicode"
"unicode/utf8"
"golang.org/x/mod/semver"
)
// A Version (for clients, a module.Version) is defined by a module path and version pair.
// These are stored in their plain (unescaped) form.
type Version struct {
// Path is a module path, like "golang.org/x/text" or "rsc.io/quote/v2".
Path string
// Version is usually a semantic version in canonical form.
// There are three exceptions to this general rule.
// First, the top-level target of a build has no specific version
// and uses Version = "".
// Second, during MVS calculations the version "none" is used
// to represent the decision to take no version of a given module.
// Third, filesystem paths found in "replace" directives are
// represented by a path with an empty version.
Version string `json:",omitempty"`
}
// String returns a representation of the Version suitable for logging
// (Path@Version, or just Path if Version is empty).
func (m Version) String() string {
if m.Version == "" {
return m.Path
}
return m.Path + "@" + m.Version
}
// A ModuleError indicates an error specific to a module.
type ModuleError struct {
Path string
Version string
Err error
}
// VersionError returns a [ModuleError] derived from a [Version] and error,
// or err itself if it is already such an error.
func VersionError(v Version, err error) error {
var mErr *ModuleError
if errors.As(err, &mErr) && mErr.Path == v.Path && mErr.Version == v.Version {
return err
}
return &ModuleError{
Path: v.Path,
Version: v.Version,
Err: err,
}
}
func (e *ModuleError) Error() string {
if v, ok := e.Err.(*InvalidVersionError); ok {
return fmt.Sprintf("%s@%s: invalid %s: %v", e.Path, v.Version, v.noun(), v.Err)
}
if e.Version != "" {
return fmt.Sprintf("%s@%s: %v", e.Path, e.Version, e.Err)
}
return fmt.Sprintf("module %s: %v", e.Path, e.Err)
}
func (e *ModuleError) Unwrap() error { return e.Err }
// An InvalidVersionError indicates an error specific to a version, with the
// module path unknown or specified externally.
//
// A [ModuleError] may wrap an InvalidVersionError, but an InvalidVersionError
// must not wrap a ModuleError.
type InvalidVersionError struct {
Version string
Pseudo bool
Err error
}
// noun returns either "version" or "pseudo-version", depending on whether
// e.Version is a pseudo-version.
func (e *InvalidVersionError) noun() string {
if e.Pseudo {
return "pseudo-version"
}
return "version"
}
func (e *InvalidVersionError) Error() string {
return fmt.Sprintf("%s %q invalid: %s", e.noun(), e.Version, e.Err)
}
func (e *InvalidVersionError) Unwrap() error { return e.Err }
// An InvalidPathError indicates a module, import, or file path doesn't
// satisfy all naming constraints. See [CheckPath], [CheckImportPath],
// and [CheckFilePath] for specific restrictions.
type InvalidPathError struct {
Kind string // "module", "import", or "file"
Path string
Err error
}
func (e *InvalidPathError) Error() string {
return fmt.Sprintf("malformed %s path %q: %v", e.Kind, e.Path, e.Err)
}
func (e *InvalidPathError) Unwrap() error { return e.Err }
// Check checks that a given module path, version pair is valid.
// In addition to the path being a valid module path
// and the version being a valid semantic version,
// the two must correspond.
// For example, the path "yaml/v2" only corresponds to
// semantic versions beginning with "v2.".
func Check(path, version string) error {
if err := CheckPath(path); err != nil {
return err
}
if !semver.IsValid(version) {
return &ModuleError{
Path: path,
Err: &InvalidVersionError{Version: version, Err: errors.New("not a semantic version")},
}
}
_, pathMajor, _ := SplitPathVersion(path)
if err := CheckPathMajor(version, pathMajor); err != nil {
return &ModuleError{Path: path, Err: err}
}
return nil
}
// firstPathOK reports whether r can appear in the first element of a module path.
// The first element of the path must be an LDH domain name, at least for now.
// To avoid case ambiguity, the domain name must be entirely lower case.
func firstPathOK(r rune) bool {
return r == '-' || r == '.' ||
'0' <= r && r <= '9' ||
'a' <= r && r <= 'z'
}
// modPathOK reports whether r can appear in a module path element.
// Paths can be ASCII letters, ASCII digits, and limited ASCII punctuation: - . _ and ~.
//
// This matches what "go get" has historically recognized in import paths,
// and avoids confusing sequences like '%20' or '+' that would change meaning
// if used in a URL.
//
// TODO(rsc): We would like to allow Unicode letters, but that requires additional
// care in the safe encoding (see "escaped paths" above).
func modPathOK(r rune) bool {
if r < utf8.RuneSelf {
return r == '-' || r == '.' || r == '_' || r == '~' ||
'0' <= r && r <= '9' ||
'A' <= r && r <= 'Z' ||
'a' <= r && r <= 'z'
}
return false
}
// importPathOK reports whether r can appear in a package import path element.
//
// Import paths are intermediate between module paths and file paths: we allow
// disallow characters that would be confusing or ambiguous as arguments to
// 'go get' (such as '@' and ' ' ), but allow certain characters that are
// otherwise-unambiguous on the command line and historically used for some
// binary names (such as '++' as a suffix for compiler binaries and wrappers).
func importPathOK(r rune) bool {
return modPathOK(r) || r == '+'
}
// fileNameOK reports whether r can appear in a file name.
// For now we allow all Unicode letters but otherwise limit to pathOK plus a few more punctuation characters.
// If we expand the set of allowed characters here, we have to
// work harder at detecting potential case-folding and normalization collisions.
// See note about "escaped paths" above.
func fileNameOK(r rune) bool {
if r < utf8.RuneSelf {
// Entire set of ASCII punctuation, from which we remove characters:
// ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~
// We disallow some shell special characters: " ' * < > ? ` |
// (Note that some of those are disallowed by the Windows file system as well.)
// We also disallow path separators / : and \ (fileNameOK is only called on path element characters).
// We allow spaces (U+0020) in file names.
const allowed = "!#$%&()+,-.=@[]^_{}~ "
if '0' <= r && r <= '9' || 'A' <= r && r <= 'Z' || 'a' <= r && r <= 'z' {
return true
}
return strings.ContainsRune(allowed, r)
}
// It may be OK to add more ASCII punctuation here, but only carefully.
// For example Windows disallows < > \, and macOS disallows :, so we must not allow those.
return unicode.IsLetter(r)
}
// CheckPath checks that a module path is valid.
// A valid module path is a valid import path, as checked by [CheckImportPath],
// with three additional constraints.
// First, the leading path element (up to the first slash, if any),
// by convention a domain name, must contain only lower-case ASCII letters,
// ASCII digits, dots (U+002E), and dashes (U+002D);
// it must contain at least one dot and cannot start with a dash.
// Second, for a final path element of the form /vN, where N looks numeric
// (ASCII digits and dots) must not begin with a leading zero, must not be /v1,
// and must not contain any dots. For paths beginning with "gopkg.in/",
// this second requirement is replaced by a requirement that the path
// follow the gopkg.in server's conventions.
// Third, no path element may begin with a dot.
func CheckPath(path string) (err error) {
defer func() {
if err != nil {
err = &InvalidPathError{Kind: "module", Path: path, Err: err}
}
}()
if err := checkPath(path, modulePath); err != nil {
return err
}
i := strings.Index(path, "/")
if i < 0 {
i = len(path)
}
if i == 0 {
return fmt.Errorf("leading slash")
}
if !strings.Contains(path[:i], ".") {
return fmt.Errorf("missing dot in first path element")
}
if path[0] == '-' {
return fmt.Errorf("leading dash in first path element")
}
for _, r := range path[:i] {
if !firstPathOK(r) {
return fmt.Errorf("invalid char %q in first path element", r)
}
}
if _, _, ok := SplitPathVersion(path); !ok {
return fmt.Errorf("invalid version")
}
return nil
}
// CheckImportPath checks that an import path is valid.
//
// A valid import path consists of one or more valid path elements
// separated by slashes (U+002F). (It must not begin with nor end in a slash.)
//
// A valid path element is a non-empty string made up of
// ASCII letters, ASCII digits, and limited ASCII punctuation: - . _ and ~.
// It must not end with a dot (U+002E), nor contain two dots in a row.
//
// The element prefix up to the first dot must not be a reserved file name
// on Windows, regardless of case (CON, com1, NuL, and so on). The element
// must not have a suffix of a tilde followed by one or more ASCII digits
// (to exclude paths elements that look like Windows short-names).
//
// CheckImportPath may be less restrictive in the future, but see the
// top-level package documentation for additional information about
// subtleties of Unicode.
func CheckImportPath(path string) error {
if err := checkPath(path, importPath); err != nil {
return &InvalidPathError{Kind: "import", Path: path, Err: err}
}
return nil
}
// pathKind indicates what kind of path we're checking. Module paths,
// import paths, and file paths have different restrictions.
type pathKind int
const (
modulePath pathKind = iota
importPath
filePath
)
// checkPath checks that a general path is valid. kind indicates what
// specific constraints should be applied.
//
// checkPath returns an error describing why the path is not valid.
// Because these checks apply to module, import, and file paths,
// and because other checks may be applied, the caller is expected to wrap
// this error with [InvalidPathError].
func checkPath(path string, kind pathKind) error {
if !utf8.ValidString(path) {
return fmt.Errorf("invalid UTF-8")
}
if path == "" {
return fmt.Errorf("empty string")
}
if path[0] == '-' && kind != filePath {
return fmt.Errorf("leading dash")
}
if strings.Contains(path, "//") {
return fmt.Errorf("double slash")
}
if path[len(path)-1] == '/' {
return fmt.Errorf("trailing slash")
}
elemStart := 0
for i, r := range path {
if r == '/' {
if err := checkElem(path[elemStart:i], kind); err != nil {
return err
}
elemStart = i + 1
}
}
if err := checkElem(path[elemStart:], kind); err != nil {
return err
}
return nil
}
// checkElem checks whether an individual path element is valid.
func checkElem(elem string, kind pathKind) error {
if elem == "" {
return fmt.Errorf("empty path element")
}
if strings.Count(elem, ".") == len(elem) {
return fmt.Errorf("invalid path element %q", elem)
}
if elem[0] == '.' && kind == modulePath {
return fmt.Errorf("leading dot in path element")
}
if elem[len(elem)-1] == '.' {
return fmt.Errorf("trailing dot in path element")
}
for _, r := range elem {
ok := false
switch kind {
case modulePath:
ok = modPathOK(r)
case importPath:
ok = importPathOK(r)
case filePath:
ok = fileNameOK(r)
default:
panic(fmt.Sprintf("internal error: invalid kind %v", kind))
}
if !ok {
return fmt.Errorf("invalid char %q", r)
}
}
// Windows disallows a bunch of path elements, sadly.
// See https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
short := elem
if i := strings.Index(short, "."); i >= 0 {
short = short[:i]
}
for _, bad := range badWindowsNames {
if strings.EqualFold(bad, short) {
return fmt.Errorf("%q disallowed as path element component on Windows", short)
}
}
if kind == filePath {
// don't check for Windows short-names in file names. They're
// only an issue for import paths.
return nil
}
// Reject path components that look like Windows short-names.
// Those usually end in a tilde followed by one or more ASCII digits.
if tilde := strings.LastIndexByte(short, '~'); tilde >= 0 && tilde < len(short)-1 {
suffix := short[tilde+1:]
suffixIsDigits := true
for _, r := range suffix {
if r < '0' || r > '9' {
suffixIsDigits = false
break
}
}
if suffixIsDigits {
return fmt.Errorf("trailing tilde and digits in path element")
}
}
return nil
}
// CheckFilePath checks that a slash-separated file path is valid.
// The definition of a valid file path is the same as the definition
// of a valid import path except that the set of allowed characters is larger:
// all Unicode letters, ASCII digits, the ASCII space character (U+0020),
// and the ASCII punctuation characters
// “!#$%&()+,-.=@[]^_{}~”.
// (The excluded punctuation characters, " * < > ? ` ' | / \ and :,
// have special meanings in certain shells or operating systems.)
//
// CheckFilePath may be less restrictive in the future, but see the
// top-level package documentation for additional information about
// subtleties of Unicode.
func CheckFilePath(path string) error {
if err := checkPath(path, filePath); err != nil {
return &InvalidPathError{Kind: "file", Path: path, Err: err}
}
return nil
}
// badWindowsNames are the reserved file path elements on Windows.
// See https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
var badWindowsNames = []string{
"CON",
"PRN",
"AUX",
"NUL",
"COM1",
"COM2",
"COM3",
"COM4",
"COM5",
"COM6",
"COM7",
"COM8",
"COM9",
"LPT1",
"LPT2",
"LPT3",
"LPT4",
"LPT5",
"LPT6",
"LPT7",
"LPT8",
"LPT9",
}
// SplitPathVersion returns prefix and major version such that prefix+pathMajor == path
// and version is either empty or "/vN" for N >= 2.
// As a special case, gopkg.in paths are recognized directly;
// they require ".vN" instead of "/vN", and for all N, not just N >= 2.
// SplitPathVersion returns with ok = false when presented with
// a path whose last path element does not satisfy the constraints
// applied by [CheckPath], such as "example.com/pkg/v1" or "example.com/pkg/v1.2".
func SplitPathVersion(path string) (prefix, pathMajor string, ok bool) {
if strings.HasPrefix(path, "gopkg.in/") {
return splitGopkgIn(path)
}
i := len(path)
dot := false
for i > 0 && ('0' <= path[i-1] && path[i-1] <= '9' || path[i-1] == '.') {
if path[i-1] == '.' {
dot = true
}
i--
}
if i <= 1 || i == len(path) || path[i-1] != 'v' || path[i-2] != '/' {
return path, "", true
}
prefix, pathMajor = path[:i-2], path[i-2:]
if dot || len(pathMajor) <= 2 || pathMajor[2] == '0' || pathMajor == "/v1" {
return path, "", false
}
return prefix, pathMajor, true
}
// splitGopkgIn is like SplitPathVersion but only for gopkg.in paths.
func splitGopkgIn(path string) (prefix, pathMajor string, ok bool) {
if !strings.HasPrefix(path, "gopkg.in/") {
return path, "", false
}
i := len(path)
if strings.HasSuffix(path, "-unstable") {
i -= len("-unstable")
}
for i > 0 && ('0' <= path[i-1] && path[i-1] <= '9') {
i--
}
if i <= 1 || path[i-1] != 'v' || path[i-2] != '.' {
// All gopkg.in paths must end in vN for some N.
return path, "", false
}
prefix, pathMajor = path[:i-2], path[i-2:]
if len(pathMajor) <= 2 || pathMajor[2] == '0' && pathMajor != ".v0" {
return path, "", false
}
return prefix, pathMajor, true
}
// MatchPathMajor reports whether the semantic version v
// matches the path major version pathMajor.
//
// MatchPathMajor returns true if and only if [CheckPathMajor] returns nil.
func MatchPathMajor(v, pathMajor string) bool {
return CheckPathMajor(v, pathMajor) == nil
}
// CheckPathMajor returns a non-nil error if the semantic version v
// does not match the path major version pathMajor.
func CheckPathMajor(v, pathMajor string) error {
// TODO(jayconrod): return errors or panic for invalid inputs. This function
// (and others) was covered by integration tests for cmd/go, and surrounding
// code protected against invalid inputs like non-canonical versions.
if strings.HasPrefix(pathMajor, ".v") && strings.HasSuffix(pathMajor, "-unstable") {
pathMajor = strings.TrimSuffix(pathMajor, "-unstable")
}
if strings.HasPrefix(v, "v0.0.0-") && pathMajor == ".v1" {
// Allow old bug in pseudo-versions that generated v0.0.0- pseudoversion for gopkg .v1.
// For example, gopkg.in/yaml.v2@v2.2.1's go.mod requires gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405.
return nil
}
m := semver.Major(v)
if pathMajor == "" {
if m == "v0" || m == "v1" || semver.Build(v) == "+incompatible" {
return nil
}
pathMajor = "v0 or v1"
} else if pathMajor[0] == '/' || pathMajor[0] == '.' {
if m == pathMajor[1:] {
return nil
}
pathMajor = pathMajor[1:]
}
return &InvalidVersionError{
Version: v,
Err: fmt.Errorf("should be %s, not %s", pathMajor, semver.Major(v)),
}
}
// PathMajorPrefix returns the major-version tag prefix implied by pathMajor.
// An empty PathMajorPrefix allows either v0 or v1.
//
// Note that [MatchPathMajor] may accept some versions that do not actually begin
// with this prefix: namely, it accepts a 'v0.0.0-' prefix for a '.v1'
// pathMajor, even though that pathMajor implies 'v1' tagging.
func PathMajorPrefix(pathMajor string) string {
if pathMajor == "" {
return ""
}
if pathMajor[0] != '/' && pathMajor[0] != '.' {
panic("pathMajor suffix " + pathMajor + " passed to PathMajorPrefix lacks separator")
}
if strings.HasPrefix(pathMajor, ".v") && strings.HasSuffix(pathMajor, "-unstable") {
pathMajor = strings.TrimSuffix(pathMajor, "-unstable")
}
m := pathMajor[1:]
if m != semver.Major(m) {
panic("pathMajor suffix " + pathMajor + "passed to PathMajorPrefix is not a valid major version")
}
return m
}
// CanonicalVersion returns the canonical form of the version string v.
// It is the same as [semver.Canonical] except that it preserves the special build suffix "+incompatible".
func CanonicalVersion(v string) string {
cv := semver.Canonical(v)
if semver.Build(v) == "+incompatible" {
cv += "+incompatible"
}
return cv
}
// Sort sorts the list by Path, breaking ties by comparing [Version] fields.
// The Version fields are interpreted as semantic versions (using [semver.Compare])
// optionally followed by a tie-breaking suffix introduced by a slash character,
// like in "v0.0.1/go.mod".
func Sort(list []Version) {
sort.Slice(list, func(i, j int) bool {
mi := list[i]
mj := list[j]
if mi.Path != mj.Path {
return mi.Path < mj.Path
}
// To help go.sum formatting, allow version/file.
// Compare semver prefix by semver rules,
// file by string order.
vi := mi.Version
vj := mj.Version
var fi, fj string
if k := strings.Index(vi, "/"); k >= 0 {
vi, fi = vi[:k], vi[k:]
}
if k := strings.Index(vj, "/"); k >= 0 {
vj, fj = vj[:k], vj[k:]
}
if vi != vj {
return semver.Compare(vi, vj) < 0
}
return fi < fj
})
}
// EscapePath returns the escaped form of the given module path.
// It fails if the module path is invalid.
func EscapePath(path string) (escaped string, err error) {
if err := CheckPath(path); err != nil {
return "", err
}
return escapeString(path)
}
// EscapeVersion returns the escaped form of the given module version.
// Versions are allowed to be in non-semver form but must be valid file names
// and not contain exclamation marks.
func EscapeVersion(v string) (escaped string, err error) {
if err := checkElem(v, filePath); err != nil || strings.Contains(v, "!") {
return "", &InvalidVersionError{
Version: v,
Err: fmt.Errorf("disallowed version string"),
}
}
return escapeString(v)
}
func escapeString(s string) (escaped string, err error) {
haveUpper := false
for _, r := range s {
if r == '!' || r >= utf8.RuneSelf {
// This should be disallowed by CheckPath, but diagnose anyway.
// The correctness of the escaping loop below depends on it.
return "", fmt.Errorf("internal error: inconsistency in EscapePath")
}
if 'A' <= r && r <= 'Z' {
haveUpper = true
}
}
if !haveUpper {
return s, nil
}
var buf []byte
for _, r := range s {
if 'A' <= r && r <= 'Z' {
buf = append(buf, '!', byte(r+'a'-'A'))
} else {
buf = append(buf, byte(r))
}
}
return string(buf), nil
}
// UnescapePath returns the module path for the given escaped path.
// It fails if the escaped path is invalid or describes an invalid path.
func UnescapePath(escaped string) (path string, err error) {
path, ok := unescapeString(escaped)
if !ok {
return "", fmt.Errorf("invalid escaped module path %q", escaped)
}
if err := CheckPath(path); err != nil {
return "", fmt.Errorf("invalid escaped module path %q: %v", escaped, err)
}
return path, nil
}
// UnescapeVersion returns the version string for the given escaped version.
// It fails if the escaped form is invalid or describes an invalid version.
// Versions are allowed to be in non-semver form but must be valid file names
// and not contain exclamation marks.
func UnescapeVersion(escaped string) (v string, err error) {
v, ok := unescapeString(escaped)
if !ok {
return "", fmt.Errorf("invalid escaped version %q", escaped)
}
if err := checkElem(v, filePath); err != nil {
return "", fmt.Errorf("invalid escaped version %q: %v", v, err)
}
return v, nil
}
func unescapeString(escaped string) (string, bool) {
var buf []byte
bang := false
for _, r := range escaped {
if r >= utf8.RuneSelf {
return "", false
}
if bang {
bang = false
if r < 'a' || 'z' < r {
return "", false
}
buf = append(buf, byte(r+'A'-'a'))
continue
}
if r == '!' {
bang = true
continue
}
if 'A' <= r && r <= 'Z' {
return "", false
}
buf = append(buf, byte(r))
}
if bang {
return "", false
}
return string(buf), true
}
// MatchPrefixPatterns reports whether any path prefix of target matches one of
// the glob patterns (as defined by [path.Match]) in the comma-separated globs
// list. This implements the algorithm used when matching a module path to the
// GOPRIVATE environment variable, as described by 'go help module-private'.
//
// It ignores any empty or malformed patterns in the list.
// Trailing slashes on patterns are ignored.
func MatchPrefixPatterns(globs, target string) bool {
for globs != "" {
// Extract next non-empty glob in comma-separated list.
var glob string
if i := strings.Index(globs, ","); i >= 0 {
glob, globs = globs[:i], globs[i+1:]
} else {
glob, globs = globs, ""
}
glob = strings.TrimSuffix(glob, "/")
if glob == "" {
continue
}
// A glob with N+1 path elements (N slashes) needs to be matched
// against the first N+1 path elements of target,
// which end just before the N+1'th slash.
n := strings.Count(glob, "/")
prefix := target
// Walk target, counting slashes, truncating at the N+1'th slash.
for i := 0; i < len(target); i++ {
if target[i] == '/' {
if n == 0 {
prefix = target[:i]
break
}
n--
}
}
if n > 0 {
// Not enough prefix elements.
continue
}
matched, _ := path.Match(glob, prefix)
if matched {
return true
}
}
return false
}
@@ -0,0 +1,389 @@
// 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"
var checkTests = []struct {
path string
version string
ok bool
}{
{"rsc.io/quote", "0.1.0", false},
{"rsc io/quote", "v1.0.0", false},
{"github.com/go-yaml/yaml", "v0.8.0", true},
{"github.com/go-yaml/yaml", "v1.0.0", true},
{"github.com/go-yaml/yaml", "v2.0.0", false},
{"github.com/go-yaml/yaml", "v2.1.5", false},
{"github.com/go-yaml/yaml", "v3.0.0", false},
{"github.com/go-yaml/yaml/v2", "v1.0.0", false},
{"github.com/go-yaml/yaml/v2", "v2.0.0", true},
{"github.com/go-yaml/yaml/v2", "v2.1.5", true},
{"github.com/go-yaml/yaml/v2", "v3.0.0", false},
{"gopkg.in/yaml.v0", "v0.8.0", true},
{"gopkg.in/yaml.v0", "v1.0.0", false},
{"gopkg.in/yaml.v0", "v2.0.0", false},
{"gopkg.in/yaml.v0", "v2.1.5", false},
{"gopkg.in/yaml.v0", "v3.0.0", false},
{"gopkg.in/yaml.v1", "v0.8.0", false},
{"gopkg.in/yaml.v1", "v1.0.0", true},
{"gopkg.in/yaml.v1", "v2.0.0", false},
{"gopkg.in/yaml.v1", "v2.1.5", false},
{"gopkg.in/yaml.v1", "v3.0.0", false},
// For gopkg.in, .v1 means v1 only (not v0).
// But early versions of vgo still generated v0 pseudo-versions for it.
// Even though now we'd generate those as v1 pseudo-versions,
// we accept the old pseudo-versions to avoid breaking existing go.mod files.
// For example gopkg.in/yaml.v2@v2.2.1's go.mod requires check.v1 at a v0 pseudo-version.
{"gopkg.in/check.v1", "v0.0.0", false},
{"gopkg.in/check.v1", "v0.0.0-20160102150405-abcdef123456", true},
{"gopkg.in/yaml.v2", "v1.0.0", false},
{"gopkg.in/yaml.v2", "v2.0.0", true},
{"gopkg.in/yaml.v2", "v2.1.5", true},
{"gopkg.in/yaml.v2", "v3.0.0", false},
{"rsc.io/quote", "v17.0.0", false},
{"rsc.io/quote", "v17.0.0+incompatible", true},
}
func TestCheck(t *testing.T) {
for _, tt := range checkTests {
err := Check(tt.path, tt.version)
if tt.ok && err != nil {
t.Errorf("Check(%q, %q) = %v, wanted nil error", tt.path, tt.version, err)
} else if !tt.ok && err == nil {
t.Errorf("Check(%q, %q) succeeded, wanted error", tt.path, tt.version)
}
}
}
var checkPathTests = []struct {
path string
ok bool
importOK bool
fileOK bool
}{
{"x.y/z", true, true, true},
{"x.y", true, true, true},
{"", false, false, false},
{"x.y/\xFFz", false, false, false},
{"/x.y/z", false, false, false},
{"x./z", false, false, false},
{".x/z", false, true, true},
{"-x/z", false, false, true},
{"x..y/z", true, true, true},
{"x.y/z/../../w", false, false, false},
{"x.y//z", false, false, false},
{"x.y/z//w", false, false, false},
{"x.y/z/", false, false, false},
{"x.y/z/v0", false, true, true},
{"x.y/z/v1", false, true, true},
{"x.y/z/v2", true, true, true},
{"x.y/z/v2.0", false, true, true},
{"X.y/z", false, true, true},
{"!x.y/z", false, false, true},
{"_x.y/z", false, true, true},
{"x.y!/z", false, false, true},
{"x.y\"/z", false, false, false},
{"x.y#/z", false, false, true},
{"x.y$/z", false, false, true},
{"x.y%/z", false, false, true},
{"x.y&/z", false, false, true},
{"x.y'/z", false, false, false},
{"x.y(/z", false, false, true},
{"x.y)/z", false, false, true},
{"x.y*/z", false, false, false},
{"x.y+/z", false, true, true},
{"x.y,/z", false, false, true},
{"x.y-/z", true, true, true},
{"x.y./zt", false, false, false},
{"x.y:/z", false, false, false},
{"x.y;/z", false, false, false},
{"x.y</z", false, false, false},
{"x.y=/z", false, false, true},
{"x.y>/z", false, false, false},
{"x.y?/z", false, false, false},
{"x.y@/z", false, false, true},
{"x.y[/z", false, false, true},
{"x.y\\/z", false, false, false},
{"x.y]/z", false, false, true},
{"x.y^/z", false, false, true},
{"x.y_/z", false, true, true},
{"x.y`/z", false, false, false},
{"x.y{/z", false, false, true},
{"x.y}/z", false, false, true},
{"x.y~/z", false, true, true},
{"x.y/z!", false, false, true},
{"x.y/z\"", false, false, false},
{"x.y/z#", false, false, true},
{"x.y/z$", false, false, true},
{"x.y/z%", false, false, true},
{"x.y/z&", false, false, true},
{"x.y/z'", false, false, false},
{"x.y/z(", false, false, true},
{"x.y/z)", false, false, true},
{"x.y/z*", false, false, false},
{"x.y/z++", false, true, true},
{"x.y/z,", false, false, true},
{"x.y/z-", true, true, true},
{"x.y/z.t", true, true, true},
{"x.y/z/t", true, true, true},
{"x.y/z:", false, false, false},
{"x.y/z;", false, false, false},
{"x.y/z<", false, false, false},
{"x.y/z=", false, false, true},
{"x.y/z>", false, false, false},
{"x.y/z?", false, false, false},
{"x.y/z@", false, false, true},
{"x.y/z[", false, false, true},
{"x.y/z\\", false, false, false},
{"x.y/z]", false, false, true},
{"x.y/z^", false, false, true},
{"x.y/z_", true, true, true},
{"x.y/z`", false, false, false},
{"x.y/z{", false, false, true},
{"x.y/z}", false, false, true},
{"x.y/z~", true, true, true},
{"x.y/x.foo", true, true, true},
{"x.y/aux.foo", false, false, false},
{"x.y/prn", false, false, false},
{"x.y/prn2", true, true, true},
{"x.y/com", true, true, true},
{"x.y/com1", false, false, false},
{"x.y/com1.txt", false, false, false},
{"x.y/calm1", true, true, true},
{"x.y/z~", true, true, true},
{"x.y/z~0", false, false, true},
{"x.y/z~09", false, false, true},
{"x.y/z09", true, true, true},
{"x.y/z09~", true, true, true},
{"x.y/z09~09z", true, true, true},
{"x.y/z09~09z~09", false, false, true},
{"github.com/!123/logrus", false, false, true},
// TODO: CL 41822 allowed Unicode letters in old "go get"
// without due consideration of the implications, and only on github.com (!).
// For now, we disallow non-ASCII characters in module mode,
// in both module paths and general import paths,
// until we can get the implications right.
// When we do, we'll enable them everywhere, not just for GitHub.
{"github.com/user/unicode/испытание", false, false, true},
{"../x", false, false, false},
{"./y", false, false, false},
{"x:y", false, false, false},
{`\temp\foo`, false, false, false},
{".gitignore", false, true, true},
{".github/ISSUE_TEMPLATE", false, true, true},
{"x☺y", false, false, false},
}
func TestCheckPath(t *testing.T) {
for _, tt := range checkPathTests {
err := CheckPath(tt.path)
if tt.ok && err != nil {
t.Errorf("CheckPath(%q) = %v, wanted nil error", tt.path, err)
} else if !tt.ok && err == nil {
t.Errorf("CheckPath(%q) succeeded, wanted error", tt.path)
}
err = CheckImportPath(tt.path)
if tt.importOK && err != nil {
t.Errorf("CheckImportPath(%q) = %v, wanted nil error", tt.path, err)
} else if !tt.importOK && err == nil {
t.Errorf("CheckImportPath(%q) succeeded, wanted error", tt.path)
}
err = CheckFilePath(tt.path)
if tt.fileOK && err != nil {
t.Errorf("CheckFilePath(%q) = %v, wanted nil error", tt.path, err)
} else if !tt.fileOK && err == nil {
t.Errorf("CheckFilePath(%q) succeeded, wanted error", tt.path)
}
}
}
var splitPathVersionTests = []struct {
pathPrefix string
version string
}{
{"x.y/z", ""},
{"x.y/z", "/v2"},
{"x.y/z", "/v3"},
{"x.y/v", ""},
{"gopkg.in/yaml", ".v0"},
{"gopkg.in/yaml", ".v1"},
{"gopkg.in/yaml", ".v2"},
{"gopkg.in/yaml", ".v3"},
}
func TestSplitPathVersion(t *testing.T) {
for _, tt := range splitPathVersionTests {
pathPrefix, version, ok := SplitPathVersion(tt.pathPrefix + tt.version)
if pathPrefix != tt.pathPrefix || version != tt.version || !ok {
t.Errorf("SplitPathVersion(%q) = %q, %q, %v, want %q, %q, true", tt.pathPrefix+tt.version, pathPrefix, version, ok, tt.pathPrefix, tt.version)
}
}
for _, tt := range checkPathTests {
pathPrefix, version, ok := SplitPathVersion(tt.path)
if pathPrefix+version != tt.path {
t.Errorf("SplitPathVersion(%q) = %q, %q, %v, doesn't add to input", tt.path, pathPrefix, version, ok)
}
}
}
var escapeTests = []struct {
path string
esc string // empty means same as path
}{
{path: "ascii.com/abcdefghijklmnopqrstuvwxyz.-/~_0123456789"},
{path: "github.com/GoogleCloudPlatform/omega", esc: "github.com/!google!cloud!platform/omega"},
}
func TestEscapePath(t *testing.T) {
// Check invalid paths.
for _, tt := range checkPathTests {
if !tt.ok {
_, err := EscapePath(tt.path)
if err == nil {
t.Errorf("EscapePath(%q): succeeded, want error (invalid path)", tt.path)
}
}
}
// Check encodings.
for _, tt := range escapeTests {
esc, err := EscapePath(tt.path)
if err != nil {
t.Errorf("EscapePath(%q): unexpected error: %v", tt.path, err)
continue
}
want := tt.esc
if want == "" {
want = tt.path
}
if esc != want {
t.Errorf("EscapePath(%q) = %q, want %q", tt.path, esc, want)
}
}
}
var badUnescape = []string{
"github.com/GoogleCloudPlatform/omega",
"github.com/!google!cloud!platform!/omega",
"github.com/!0google!cloud!platform/omega",
"github.com/!_google!cloud!platform/omega",
"github.com/!!google!cloud!platform/omega",
"",
}
func TestUnescapePath(t *testing.T) {
// Check invalid decodings.
for _, bad := range badUnescape {
_, err := UnescapePath(bad)
if err == nil {
t.Errorf("UnescapePath(%q): succeeded, want error (invalid decoding)", bad)
}
}
// Check invalid paths (or maybe decodings).
for _, tt := range checkPathTests {
if !tt.ok {
path, err := UnescapePath(tt.path)
if err == nil {
t.Errorf("UnescapePath(%q) = %q, want error (invalid path)", tt.path, path)
}
}
}
// Check encodings.
for _, tt := range escapeTests {
esc := tt.esc
if esc == "" {
esc = tt.path
}
path, err := UnescapePath(esc)
if err != nil {
t.Errorf("UnescapePath(%q): unexpected error: %v", esc, err)
continue
}
if path != tt.path {
t.Errorf("UnescapePath(%q) = %q, want %q", esc, path, tt.path)
}
}
}
func TestMatchPathMajor(t *testing.T) {
for _, test := range []struct {
v, pathMajor string
want bool
}{
{"v0.0.0", "", true},
{"v0.0.0", "/v2", false},
{"v0.0.0", ".v0", true},
{"v0.0.0-20190510104115-cbcb75029529", ".v1", true},
{"v1.0.0", "/v2", false},
{"v1.0.0", ".v1", true},
{"v1.0.0", ".v1-unstable", true},
{"v2.0.0+incompatible", "", true},
{"v2.0.0", "", false},
{"v2.0.0", "/v2", true},
{"v2.0.0", ".v2", true},
} {
if got := MatchPathMajor(test.v, test.pathMajor); got != test.want {
t.Errorf("MatchPathMajor(%q, %q) = %v, want %v", test.v, test.pathMajor, got, test.want)
}
}
}
func TestMatchPrefixPatterns(t *testing.T) {
for _, test := range []struct {
globs, target string
want bool
}{
{"", "rsc.io/quote", false},
{"/", "rsc.io/quote", false},
{"*/quote", "rsc.io/quote", true},
{"*/quo", "rsc.io/quote", false},
{"*/quo??", "rsc.io/quote", true},
{"*/quo*", "rsc.io/quote", true},
{"*quo*", "rsc.io/quote", false},
{"rsc.io", "rsc.io/quote", true},
{"*.io", "rsc.io/quote", true},
{"rsc.io/", "rsc.io/quote", true},
{"rsc", "rsc.io/quote", false},
{"rsc*", "rsc.io/quote", true},
{"rsc.io", "rsc.io/quote/v3", true},
{"*/quote", "rsc.io/quote/v3", true},
{"*/quote/", "rsc.io/quote/v3", true},
{"*/quote/*", "rsc.io/quote/v3", true},
{"*/quote/*/", "rsc.io/quote/v3", true},
{"*/v3", "rsc.io/quote/v3", false},
{"*/*/v3", "rsc.io/quote/v3", true},
{"*/*/*", "rsc.io/quote/v3", true},
{"*/*/*/", "rsc.io/quote/v3", true},
{"*/*/*", "rsc.io/quote", false},
{"*/*/*/", "rsc.io/quote", false},
{"*/*/*,,", "rsc.io/quote", false},
{"*/*/*,,*/quote", "rsc.io/quote", true},
{",,*/quote", "rsc.io/quote", true},
} {
if got := MatchPrefixPatterns(test.globs, test.target); got != test.want {
t.Errorf("MatchPrefixPatterns(%q, %q) = %t, want %t", test.globs, test.target, got, test.want)
}
}
}
@@ -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
}
@@ -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)
}
}
}