Source file src/cmd/vendor/golang.org/x/mod/module/module.go

     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  // Package module defines the module.Version type along with support code.
     6  //
     7  // The [module.Version] type is a simple Path, Version pair:
     8  //
     9  //	type Version struct {
    10  //		Path string
    11  //		Version string
    12  //	}
    13  //
    14  // There are no restrictions imposed directly by use of this structure,
    15  // but additional checking functions, most notably [Check], verify that
    16  // a particular path, version pair is valid.
    17  //
    18  // # Escaped Paths
    19  //
    20  // Module paths appear as substrings of file system paths
    21  // (in the download cache) and of web server URLs in the proxy protocol.
    22  // In general we cannot rely on file systems to be case-sensitive,
    23  // nor can we rely on web servers, since they read from file systems.
    24  // That is, we cannot rely on the file system to keep rsc.io/QUOTE
    25  // and rsc.io/quote separate. Windows and macOS don't.
    26  // Instead, we must never require two different casings of a file path.
    27  // Because we want the download cache to match the proxy protocol,
    28  // and because we want the proxy protocol to be possible to serve
    29  // from a tree of static files (which might be stored on a case-insensitive
    30  // file system), the proxy protocol must never require two different casings
    31  // of a URL path either.
    32  //
    33  // One possibility would be to make the escaped form be the lowercase
    34  // hexadecimal encoding of the actual path bytes. This would avoid ever
    35  // needing different casings of a file path, but it would be fairly illegible
    36  // to most programmers when those paths appeared in the file system
    37  // (including in file paths in compiler errors and stack traces)
    38  // in web server logs, and so on. Instead, we want a safe escaped form that
    39  // leaves most paths unaltered.
    40  //
    41  // The safe escaped form is to replace every uppercase letter
    42  // with an exclamation mark followed by the letter's lowercase equivalent.
    43  //
    44  // For example,
    45  //
    46  //	github.com/Azure/azure-sdk-for-go ->  github.com/!azure/azure-sdk-for-go.
    47  //	github.com/GoogleCloudPlatform/cloudsql-proxy -> github.com/!google!cloud!platform/cloudsql-proxy
    48  //	github.com/Sirupsen/logrus -> github.com/!sirupsen/logrus.
    49  //
    50  // Import paths that avoid upper-case letters are left unchanged.
    51  // Note that because import paths are ASCII-only and avoid various
    52  // problematic punctuation (like : < and >), the escaped form is also ASCII-only
    53  // and avoids the same problematic punctuation.
    54  //
    55  // Import paths have never allowed exclamation marks, so there is no
    56  // need to define how to escape a literal !.
    57  //
    58  // # Unicode Restrictions
    59  //
    60  // Today, paths are disallowed from using Unicode.
    61  //
    62  // Although paths are currently disallowed from using Unicode,
    63  // we would like at some point to allow Unicode letters as well, to assume that
    64  // file systems and URLs are Unicode-safe (storing UTF-8), and apply
    65  // the !-for-uppercase convention for escaping them in the file system.
    66  // But there are at least two subtle considerations.
    67  //
    68  // First, note that not all case-fold equivalent distinct runes
    69  // form an upper/lower pair.
    70  // For example, U+004B ('K'), U+006B ('k'), and U+212A ('K' for Kelvin)
    71  // are three distinct runes that case-fold to each other.
    72  // When we do add Unicode letters, we must not assume that upper/lower
    73  // are the only case-equivalent pairs.
    74  // Perhaps the Kelvin symbol would be disallowed entirely, for example.
    75  // Or perhaps it would escape as "!!k", or perhaps as "(212A)".
    76  //
    77  // Second, it would be nice to allow Unicode marks as well as letters,
    78  // but marks include combining marks, and then we must deal not
    79  // only with case folding but also normalization: both U+00E9 ('é')
    80  // and U+0065 U+0301 ('e' followed by combining acute accent)
    81  // look the same on the page and are treated by some file systems
    82  // as the same path. If we do allow Unicode marks in paths, there
    83  // must be some kind of normalization to allow only one canonical
    84  // encoding of any character used in an import path.
    85  package module
    86  
    87  // IMPORTANT NOTE
    88  //
    89  // This file essentially defines the set of valid import paths for the go command.
    90  // There are many subtle considerations, including Unicode ambiguity,
    91  // security, network, and file system representations.
    92  //
    93  // This file also defines the set of valid module path and version combinations,
    94  // another topic with many subtle considerations.
    95  //
    96  // Changes to the semantics in this file require approval from rsc.
    97  
    98  import (
    99  	"cmp"
   100  	"errors"
   101  	"fmt"
   102  	"path"
   103  	"slices"
   104  	"strings"
   105  	"unicode"
   106  	"unicode/utf8"
   107  
   108  	"golang.org/x/mod/semver"
   109  )
   110  
   111  // A Version (for clients, a module.Version) is defined by a module path and version pair.
   112  // These are stored in their plain (unescaped) form.
   113  type Version struct {
   114  	// Path is a module path, like "golang.org/x/text" or "rsc.io/quote/v2".
   115  	Path string
   116  
   117  	// Version is usually a semantic version in canonical form.
   118  	// There are three exceptions to this general rule.
   119  	// First, the top-level target of a build has no specific version
   120  	// and uses Version = "".
   121  	// Second, during MVS calculations the version "none" is used
   122  	// to represent the decision to take no version of a given module.
   123  	// Third, filesystem paths found in "replace" directives are
   124  	// represented by a path with an empty version.
   125  	Version string `json:",omitempty"`
   126  }
   127  
   128  // String returns a representation of the Version suitable for logging
   129  // (Path@Version, or just Path if Version is empty).
   130  func (m Version) String() string {
   131  	if m.Version == "" {
   132  		return m.Path
   133  	}
   134  	return m.Path + "@" + m.Version
   135  }
   136  
   137  // A ModuleError indicates an error specific to a module.
   138  type ModuleError struct {
   139  	Path    string
   140  	Version string
   141  	Err     error
   142  }
   143  
   144  // VersionError returns a [ModuleError] derived from a [Version] and error,
   145  // or err itself if it is already such an error.
   146  func VersionError(v Version, err error) error {
   147  	var mErr *ModuleError
   148  	if errors.As(err, &mErr) && mErr.Path == v.Path && mErr.Version == v.Version {
   149  		return err
   150  	}
   151  	return &ModuleError{
   152  		Path:    v.Path,
   153  		Version: v.Version,
   154  		Err:     err,
   155  	}
   156  }
   157  
   158  func (e *ModuleError) Error() string {
   159  	if v, ok := e.Err.(*InvalidVersionError); ok {
   160  		return fmt.Sprintf("%s@%s: invalid %s: %v", e.Path, v.Version, v.noun(), v.Err)
   161  	}
   162  	if e.Version != "" {
   163  		return fmt.Sprintf("%s@%s: %v", e.Path, e.Version, e.Err)
   164  	}
   165  	return fmt.Sprintf("module %s: %v", e.Path, e.Err)
   166  }
   167  
   168  func (e *ModuleError) Unwrap() error { return e.Err }
   169  
   170  // An InvalidVersionError indicates an error specific to a version, with the
   171  // module path unknown or specified externally.
   172  //
   173  // A [ModuleError] may wrap an InvalidVersionError, but an InvalidVersionError
   174  // must not wrap a ModuleError.
   175  type InvalidVersionError struct {
   176  	Version string
   177  	Pseudo  bool
   178  	Err     error
   179  }
   180  
   181  // noun returns either "version" or "pseudo-version", depending on whether
   182  // e.Version is a pseudo-version.
   183  func (e *InvalidVersionError) noun() string {
   184  	if e.Pseudo {
   185  		return "pseudo-version"
   186  	}
   187  	return "version"
   188  }
   189  
   190  func (e *InvalidVersionError) Error() string {
   191  	return fmt.Sprintf("%s %q invalid: %s", e.noun(), e.Version, e.Err)
   192  }
   193  
   194  func (e *InvalidVersionError) Unwrap() error { return e.Err }
   195  
   196  // An InvalidPathError indicates a module, import, or file path doesn't
   197  // satisfy all naming constraints. See [CheckPath], [CheckImportPath],
   198  // and [CheckFilePath] for specific restrictions.
   199  type InvalidPathError struct {
   200  	Kind string // "module", "import", or "file"
   201  	Path string
   202  	Err  error
   203  }
   204  
   205  func (e *InvalidPathError) Error() string {
   206  	return fmt.Sprintf("malformed %s path %q: %v", e.Kind, e.Path, e.Err)
   207  }
   208  
   209  func (e *InvalidPathError) Unwrap() error { return e.Err }
   210  
   211  // Check checks that a given module path, version pair is valid.
   212  // In addition to the path being a valid module path
   213  // and the version being a valid semantic version,
   214  // the two must correspond.
   215  // For example, the path "yaml/v2" only corresponds to
   216  // semantic versions beginning with "v2.".
   217  func Check(path, version string) error {
   218  	if err := CheckPath(path); err != nil {
   219  		return err
   220  	}
   221  	if !semver.IsValid(version) {
   222  		return &ModuleError{
   223  			Path: path,
   224  			Err:  &InvalidVersionError{Version: version, Err: errors.New("not a semantic version")},
   225  		}
   226  	}
   227  	_, pathMajor, _ := SplitPathVersion(path)
   228  	if err := CheckPathMajor(version, pathMajor); err != nil {
   229  		return &ModuleError{Path: path, Err: err}
   230  	}
   231  	return nil
   232  }
   233  
   234  // firstPathOK reports whether r can appear in the first element of a module path.
   235  // The first element of the path must be an LDH domain name, at least for now.
   236  // To avoid case ambiguity, the domain name must be entirely lower case.
   237  func firstPathOK(r rune) bool {
   238  	return r == '-' || r == '.' ||
   239  		'0' <= r && r <= '9' ||
   240  		'a' <= r && r <= 'z'
   241  }
   242  
   243  // modPathOK reports whether r can appear in a module path element.
   244  // Paths can be ASCII letters, ASCII digits, and limited ASCII punctuation: - . _ and ~.
   245  //
   246  // This matches what "go get" has historically recognized in import paths,
   247  // and avoids confusing sequences like '%20' or '+' that would change meaning
   248  // if used in a URL.
   249  //
   250  // TODO(rsc): We would like to allow Unicode letters, but that requires additional
   251  // care in the safe encoding (see "escaped paths" above).
   252  func modPathOK(r rune) bool {
   253  	if r < utf8.RuneSelf {
   254  		return r == '-' || r == '.' || r == '_' || r == '~' ||
   255  			'0' <= r && r <= '9' ||
   256  			'A' <= r && r <= 'Z' ||
   257  			'a' <= r && r <= 'z'
   258  	}
   259  	return false
   260  }
   261  
   262  // importPathOK reports whether r can appear in a package import path element.
   263  //
   264  // Import paths are intermediate between module paths and file paths: we allow
   265  // disallow characters that would be confusing or ambiguous as arguments to
   266  // 'go get' (such as '@' and ' ' ), but allow certain characters that are
   267  // otherwise-unambiguous on the command line and historically used for some
   268  // binary names (such as '++' as a suffix for compiler binaries and wrappers).
   269  func importPathOK(r rune) bool {
   270  	return modPathOK(r) || r == '+'
   271  }
   272  
   273  // fileNameOK reports whether r can appear in a file name.
   274  // For now we allow all Unicode letters but otherwise limit to pathOK plus a few more punctuation characters.
   275  // If we expand the set of allowed characters here, we have to
   276  // work harder at detecting potential case-folding and normalization collisions.
   277  // See note about "escaped paths" above.
   278  func fileNameOK(r rune) bool {
   279  	if r < utf8.RuneSelf {
   280  		// Entire set of ASCII punctuation, from which we remove characters:
   281  		//     ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~
   282  		// We disallow some shell special characters: " ' * < > ? ` |
   283  		// (Note that some of those are disallowed by the Windows file system as well.)
   284  		// We also disallow path separators / : and \ (fileNameOK is only called on path element characters).
   285  		// We allow spaces (U+0020) in file names.
   286  		const allowed = "!#$%&()+,-.=@[]^_{}~ "
   287  		if '0' <= r && r <= '9' || 'A' <= r && r <= 'Z' || 'a' <= r && r <= 'z' {
   288  			return true
   289  		}
   290  		return strings.ContainsRune(allowed, r)
   291  	}
   292  	// It may be OK to add more ASCII punctuation here, but only carefully.
   293  	// For example Windows disallows < > \, and macOS disallows :, so we must not allow those.
   294  	return unicode.IsLetter(r)
   295  }
   296  
   297  // CheckPath checks that a module path is valid.
   298  // A valid module path is a valid import path, as checked by [CheckImportPath],
   299  // with three additional constraints.
   300  // First, the leading path element (up to the first slash, if any),
   301  // by convention a domain name, must contain only lower-case ASCII letters,
   302  // ASCII digits, dots (U+002E), and dashes (U+002D);
   303  // it must contain at least one dot and cannot start with a dash.
   304  // Second, for a final path element of the form /vN, where N looks numeric
   305  // (ASCII digits and dots) must not begin with a leading zero, must not be /v1,
   306  // and must not contain any dots. For paths beginning with "gopkg.in/",
   307  // this second requirement is replaced by a requirement that the path
   308  // follow the gopkg.in server's conventions.
   309  // Third, no path element may begin with a dot.
   310  func CheckPath(path string) (err error) {
   311  	defer func() {
   312  		if err != nil {
   313  			err = &InvalidPathError{Kind: "module", Path: path, Err: err}
   314  		}
   315  	}()
   316  
   317  	if err := checkPath(path, modulePath); err != nil {
   318  		return err
   319  	}
   320  	i := strings.Index(path, "/")
   321  	if i < 0 {
   322  		i = len(path)
   323  	}
   324  	if i == 0 {
   325  		return fmt.Errorf("leading slash")
   326  	}
   327  	if !strings.Contains(path[:i], ".") {
   328  		return fmt.Errorf("missing dot in first path element")
   329  	}
   330  	if path[0] == '-' {
   331  		return fmt.Errorf("leading dash in first path element")
   332  	}
   333  	for _, r := range path[:i] {
   334  		if !firstPathOK(r) {
   335  			return fmt.Errorf("invalid char %q in first path element", r)
   336  		}
   337  	}
   338  	if _, _, ok := SplitPathVersion(path); !ok {
   339  		return fmt.Errorf("invalid version")
   340  	}
   341  	return nil
   342  }
   343  
   344  // CheckImportPath checks that an import path is valid.
   345  //
   346  // A valid import path consists of one or more valid path elements
   347  // separated by slashes (U+002F). (It must not begin with nor end in a slash.)
   348  //
   349  // A valid path element is a non-empty string made up of
   350  // ASCII letters, ASCII digits, and limited ASCII punctuation: - . _ and ~.
   351  // It must not end with a dot (U+002E), nor contain two dots in a row.
   352  //
   353  // The element prefix up to the first dot must not be a reserved file name
   354  // on Windows, regardless of case (CON, com1, NuL, and so on). The element
   355  // must not have a suffix of a tilde followed by one or more ASCII digits
   356  // (to exclude paths elements that look like Windows short-names).
   357  //
   358  // CheckImportPath may be less restrictive in the future, but see the
   359  // top-level package documentation for additional information about
   360  // subtleties of Unicode.
   361  func CheckImportPath(path string) error {
   362  	if err := checkPath(path, importPath); err != nil {
   363  		return &InvalidPathError{Kind: "import", Path: path, Err: err}
   364  	}
   365  	return nil
   366  }
   367  
   368  // pathKind indicates what kind of path we're checking. Module paths,
   369  // import paths, and file paths have different restrictions.
   370  type pathKind int
   371  
   372  const (
   373  	modulePath pathKind = iota
   374  	importPath
   375  	filePath
   376  )
   377  
   378  // checkPath checks that a general path is valid. kind indicates what
   379  // specific constraints should be applied.
   380  //
   381  // checkPath returns an error describing why the path is not valid.
   382  // Because these checks apply to module, import, and file paths,
   383  // and because other checks may be applied, the caller is expected to wrap
   384  // this error with [InvalidPathError].
   385  func checkPath(path string, kind pathKind) error {
   386  	if !utf8.ValidString(path) {
   387  		return fmt.Errorf("invalid UTF-8")
   388  	}
   389  	if path == "" {
   390  		return fmt.Errorf("empty string")
   391  	}
   392  	if path[0] == '-' && kind != filePath {
   393  		return fmt.Errorf("leading dash")
   394  	}
   395  	if strings.Contains(path, "//") {
   396  		return fmt.Errorf("double slash")
   397  	}
   398  	if path[len(path)-1] == '/' {
   399  		return fmt.Errorf("trailing slash")
   400  	}
   401  	elemStart := 0
   402  	for i, r := range path {
   403  		if r == '/' {
   404  			if err := checkElem(path[elemStart:i], kind); err != nil {
   405  				return err
   406  			}
   407  			elemStart = i + 1
   408  		}
   409  	}
   410  	if err := checkElem(path[elemStart:], kind); err != nil {
   411  		return err
   412  	}
   413  	return nil
   414  }
   415  
   416  // checkElem checks whether an individual path element is valid.
   417  func checkElem(elem string, kind pathKind) error {
   418  	if elem == "" {
   419  		return fmt.Errorf("empty path element")
   420  	}
   421  	if strings.Count(elem, ".") == len(elem) {
   422  		return fmt.Errorf("invalid path element %q", elem)
   423  	}
   424  	if elem[0] == '.' && kind == modulePath {
   425  		return fmt.Errorf("leading dot in path element")
   426  	}
   427  	if elem[len(elem)-1] == '.' {
   428  		return fmt.Errorf("trailing dot in path element")
   429  	}
   430  	for _, r := range elem {
   431  		ok := false
   432  		switch kind {
   433  		case modulePath:
   434  			ok = modPathOK(r)
   435  		case importPath:
   436  			ok = importPathOK(r)
   437  		case filePath:
   438  			ok = fileNameOK(r)
   439  		default:
   440  			panic(fmt.Sprintf("internal error: invalid kind %v", kind))
   441  		}
   442  		if !ok {
   443  			return fmt.Errorf("invalid char %q", r)
   444  		}
   445  	}
   446  
   447  	// Windows disallows a bunch of path elements, sadly.
   448  	// See https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
   449  	short := elem
   450  	if i := strings.Index(short, "."); i >= 0 {
   451  		short = short[:i]
   452  	}
   453  	for _, bad := range badWindowsNames {
   454  		if strings.EqualFold(bad, short) {
   455  			return fmt.Errorf("%q disallowed as path element component on Windows", short)
   456  		}
   457  	}
   458  
   459  	if kind == filePath {
   460  		// don't check for Windows short-names in file names. They're
   461  		// only an issue for import paths.
   462  		return nil
   463  	}
   464  
   465  	// Reject path components that look like Windows short-names.
   466  	// Those usually end in a tilde followed by one or more ASCII digits.
   467  	if tilde := strings.LastIndexByte(short, '~'); tilde >= 0 && tilde < len(short)-1 {
   468  		suffix := short[tilde+1:]
   469  		suffixIsDigits := true
   470  		for _, r := range suffix {
   471  			if r < '0' || r > '9' {
   472  				suffixIsDigits = false
   473  				break
   474  			}
   475  		}
   476  		if suffixIsDigits {
   477  			return fmt.Errorf("trailing tilde and digits in path element")
   478  		}
   479  	}
   480  
   481  	return nil
   482  }
   483  
   484  // CheckFilePath checks that a slash-separated file path is valid.
   485  // The definition of a valid file path is the same as the definition
   486  // of a valid import path except that the set of allowed characters is larger:
   487  // all Unicode letters, ASCII digits, the ASCII space character (U+0020),
   488  // and the ASCII punctuation characters
   489  // “!#$%&()+,-.=@[]^_{}~”.
   490  // (The excluded punctuation characters, " * < > ? ` ' | / \ and :,
   491  // have special meanings in certain shells or operating systems.)
   492  //
   493  // CheckFilePath may be less restrictive in the future, but see the
   494  // top-level package documentation for additional information about
   495  // subtleties of Unicode.
   496  func CheckFilePath(path string) error {
   497  	if err := checkPath(path, filePath); err != nil {
   498  		return &InvalidPathError{Kind: "file", Path: path, Err: err}
   499  	}
   500  	return nil
   501  }
   502  
   503  // badWindowsNames are the reserved file path elements on Windows.
   504  // See https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
   505  var badWindowsNames = []string{
   506  	"CON",
   507  	"PRN",
   508  	"AUX",
   509  	"NUL",
   510  	"COM1",
   511  	"COM2",
   512  	"COM3",
   513  	"COM4",
   514  	"COM5",
   515  	"COM6",
   516  	"COM7",
   517  	"COM8",
   518  	"COM9",
   519  	"LPT1",
   520  	"LPT2",
   521  	"LPT3",
   522  	"LPT4",
   523  	"LPT5",
   524  	"LPT6",
   525  	"LPT7",
   526  	"LPT8",
   527  	"LPT9",
   528  }
   529  
   530  // SplitPathVersion returns prefix and major version such that prefix+pathMajor == path
   531  // and version is either empty or "/vN" for N >= 2.
   532  // As a special case, gopkg.in paths are recognized directly;
   533  // they require ".vN" instead of "/vN", and for all N, not just N >= 2.
   534  // SplitPathVersion returns with ok = false when presented with
   535  // a path whose last path element does not satisfy the constraints
   536  // applied by [CheckPath], such as "example.com/pkg/v1" or "example.com/pkg/v1.2".
   537  func SplitPathVersion(path string) (prefix, pathMajor string, ok bool) {
   538  	if strings.HasPrefix(path, "gopkg.in/") {
   539  		return splitGopkgIn(path)
   540  	}
   541  
   542  	i := len(path)
   543  	dot := false
   544  	for i > 0 && ('0' <= path[i-1] && path[i-1] <= '9' || path[i-1] == '.') {
   545  		if path[i-1] == '.' {
   546  			dot = true
   547  		}
   548  		i--
   549  	}
   550  	if i <= 1 || i == len(path) || path[i-1] != 'v' || path[i-2] != '/' {
   551  		return path, "", true
   552  	}
   553  	prefix, pathMajor = path[:i-2], path[i-2:]
   554  	if dot || len(pathMajor) <= 2 || pathMajor[2] == '0' || pathMajor == "/v1" {
   555  		return path, "", false
   556  	}
   557  	return prefix, pathMajor, true
   558  }
   559  
   560  // splitGopkgIn is like SplitPathVersion but only for gopkg.in paths.
   561  func splitGopkgIn(path string) (prefix, pathMajor string, ok bool) {
   562  	if !strings.HasPrefix(path, "gopkg.in/") {
   563  		return path, "", false
   564  	}
   565  	i := len(path)
   566  	if strings.HasSuffix(path, "-unstable") {
   567  		i -= len("-unstable")
   568  	}
   569  	for i > 0 && ('0' <= path[i-1] && path[i-1] <= '9') {
   570  		i--
   571  	}
   572  	if i <= 1 || path[i-1] != 'v' || path[i-2] != '.' {
   573  		// All gopkg.in paths must end in vN for some N.
   574  		return path, "", false
   575  	}
   576  	prefix, pathMajor = path[:i-2], path[i-2:]
   577  	if len(pathMajor) <= 2 || pathMajor[2] == '0' && pathMajor != ".v0" {
   578  		return path, "", false
   579  	}
   580  	return prefix, pathMajor, true
   581  }
   582  
   583  // MatchPathMajor reports whether the semantic version v
   584  // matches the path major version pathMajor.
   585  //
   586  // MatchPathMajor returns true if and only if [CheckPathMajor] returns nil.
   587  func MatchPathMajor(v, pathMajor string) bool {
   588  	return CheckPathMajor(v, pathMajor) == nil
   589  }
   590  
   591  // CheckPathMajor returns a non-nil error if the semantic version v
   592  // does not match the path major version pathMajor.
   593  func CheckPathMajor(v, pathMajor string) error {
   594  	// TODO(jayconrod): return errors or panic for invalid inputs. This function
   595  	// (and others) was covered by integration tests for cmd/go, and surrounding
   596  	// code protected against invalid inputs like non-canonical versions.
   597  	if strings.HasPrefix(pathMajor, ".v") && strings.HasSuffix(pathMajor, "-unstable") {
   598  		pathMajor = strings.TrimSuffix(pathMajor, "-unstable")
   599  	}
   600  	if strings.HasPrefix(v, "v0.0.0-") && pathMajor == ".v1" {
   601  		// Allow old bug in pseudo-versions that generated v0.0.0- pseudoversion for gopkg .v1.
   602  		// For example, gopkg.in/yaml.v2@v2.2.1's go.mod requires gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405.
   603  		return nil
   604  	}
   605  	m := semver.Major(v)
   606  	if pathMajor == "" {
   607  		if m == "v0" || m == "v1" || semver.Build(v) == "+incompatible" {
   608  			return nil
   609  		}
   610  		pathMajor = "v0 or v1"
   611  	} else if pathMajor[0] == '/' || pathMajor[0] == '.' {
   612  		if m == pathMajor[1:] {
   613  			return nil
   614  		}
   615  		pathMajor = pathMajor[1:]
   616  	}
   617  	return &InvalidVersionError{
   618  		Version: v,
   619  		Err:     fmt.Errorf("should be %s, not %s", pathMajor, semver.Major(v)),
   620  	}
   621  }
   622  
   623  // PathMajorPrefix returns the major-version tag prefix implied by pathMajor.
   624  // An empty PathMajorPrefix allows either v0 or v1.
   625  //
   626  // Note that [MatchPathMajor] may accept some versions that do not actually begin
   627  // with this prefix: namely, it accepts a 'v0.0.0-' prefix for a '.v1'
   628  // pathMajor, even though that pathMajor implies 'v1' tagging.
   629  func PathMajorPrefix(pathMajor string) string {
   630  	if pathMajor == "" {
   631  		return ""
   632  	}
   633  	if pathMajor[0] != '/' && pathMajor[0] != '.' {
   634  		panic("pathMajor suffix " + pathMajor + " passed to PathMajorPrefix lacks separator")
   635  	}
   636  	if strings.HasPrefix(pathMajor, ".v") && strings.HasSuffix(pathMajor, "-unstable") {
   637  		pathMajor = strings.TrimSuffix(pathMajor, "-unstable")
   638  	}
   639  	m := pathMajor[1:]
   640  	if m != semver.Major(m) {
   641  		panic("pathMajor suffix " + pathMajor + "passed to PathMajorPrefix is not a valid major version")
   642  	}
   643  	return m
   644  }
   645  
   646  // CanonicalVersion returns the canonical form of the version string v.
   647  // It is the same as [semver.Canonical] except that it preserves the special build suffix "+incompatible".
   648  func CanonicalVersion(v string) string {
   649  	cv := semver.Canonical(v)
   650  	if semver.Build(v) == "+incompatible" {
   651  		cv += "+incompatible"
   652  	}
   653  	return cv
   654  }
   655  
   656  // Sort sorts the list by Path, breaking ties by comparing [Version] fields.
   657  // The Version fields are interpreted as semantic versions (using [semver.Compare])
   658  // optionally followed by a tie-breaking suffix introduced by a slash character,
   659  // like in "v0.0.1/go.mod".
   660  func Sort(list []Version) {
   661  	slices.SortFunc(list, func(i, j Version) int {
   662  		if i.Path != j.Path {
   663  			return strings.Compare(i.Path, j.Path)
   664  		}
   665  		// To help go.sum formatting, allow version/file.
   666  		// Compare semver prefix by semver rules,
   667  		// file by string order.
   668  		vi := i.Version
   669  		vj := j.Version
   670  		var fi, fj string
   671  		if k := strings.Index(vi, "/"); k >= 0 {
   672  			vi, fi = vi[:k], vi[k:]
   673  		}
   674  		if k := strings.Index(vj, "/"); k >= 0 {
   675  			vj, fj = vj[:k], vj[k:]
   676  		}
   677  		if vi != vj {
   678  			return semver.Compare(vi, vj)
   679  		}
   680  		return cmp.Compare(fi, fj)
   681  	})
   682  }
   683  
   684  // EscapePath returns the escaped form of the given module path.
   685  // It fails if the module path is invalid.
   686  func EscapePath(path string) (escaped string, err error) {
   687  	if err := CheckPath(path); err != nil {
   688  		return "", err
   689  	}
   690  
   691  	return escapeString(path)
   692  }
   693  
   694  // EscapeVersion returns the escaped form of the given module version.
   695  // Versions are allowed to be in non-semver form but must be valid file names
   696  // and not contain exclamation marks.
   697  func EscapeVersion(v string) (escaped string, err error) {
   698  	if err := checkElem(v, filePath); err != nil || strings.Contains(v, "!") {
   699  		return "", &InvalidVersionError{
   700  			Version: v,
   701  			Err:     fmt.Errorf("disallowed version string"),
   702  		}
   703  	}
   704  	return escapeString(v)
   705  }
   706  
   707  func escapeString(s string) (escaped string, err error) {
   708  	haveUpper := false
   709  	for _, r := range s {
   710  		if r == '!' || r >= utf8.RuneSelf {
   711  			// This should be disallowed by CheckPath, but diagnose anyway.
   712  			// The correctness of the escaping loop below depends on it.
   713  			return "", fmt.Errorf("internal error: inconsistency in EscapePath")
   714  		}
   715  		if 'A' <= r && r <= 'Z' {
   716  			haveUpper = true
   717  		}
   718  	}
   719  
   720  	if !haveUpper {
   721  		return s, nil
   722  	}
   723  
   724  	var buf []byte
   725  	for _, r := range s {
   726  		if 'A' <= r && r <= 'Z' {
   727  			buf = append(buf, '!', byte(r+'a'-'A'))
   728  		} else {
   729  			buf = append(buf, byte(r))
   730  		}
   731  	}
   732  	return string(buf), nil
   733  }
   734  
   735  // UnescapePath returns the module path for the given escaped path.
   736  // It fails if the escaped path is invalid or describes an invalid path.
   737  func UnescapePath(escaped string) (path string, err error) {
   738  	path, ok := unescapeString(escaped)
   739  	if !ok {
   740  		return "", fmt.Errorf("invalid escaped module path %q", escaped)
   741  	}
   742  	if err := CheckPath(path); err != nil {
   743  		return "", fmt.Errorf("invalid escaped module path %q: %v", escaped, err)
   744  	}
   745  	return path, nil
   746  }
   747  
   748  // UnescapeVersion returns the version string for the given escaped version.
   749  // It fails if the escaped form is invalid or describes an invalid version.
   750  // Versions are allowed to be in non-semver form but must be valid file names
   751  // and not contain exclamation marks.
   752  func UnescapeVersion(escaped string) (v string, err error) {
   753  	v, ok := unescapeString(escaped)
   754  	if !ok {
   755  		return "", fmt.Errorf("invalid escaped version %q", escaped)
   756  	}
   757  	if err := checkElem(v, filePath); err != nil {
   758  		return "", fmt.Errorf("invalid escaped version %q: %v", v, err)
   759  	}
   760  	return v, nil
   761  }
   762  
   763  func unescapeString(escaped string) (string, bool) {
   764  	var buf []byte
   765  
   766  	bang := false
   767  	for _, r := range escaped {
   768  		if r >= utf8.RuneSelf {
   769  			return "", false
   770  		}
   771  		if bang {
   772  			bang = false
   773  			if r < 'a' || 'z' < r {
   774  				return "", false
   775  			}
   776  			buf = append(buf, byte(r+'A'-'a'))
   777  			continue
   778  		}
   779  		if r == '!' {
   780  			bang = true
   781  			continue
   782  		}
   783  		if 'A' <= r && r <= 'Z' {
   784  			return "", false
   785  		}
   786  		buf = append(buf, byte(r))
   787  	}
   788  	if bang {
   789  		return "", false
   790  	}
   791  	return string(buf), true
   792  }
   793  
   794  // MatchPrefixPatterns reports whether any path prefix of target matches one of
   795  // the glob patterns (as defined by [path.Match]) in the comma-separated globs
   796  // list. This implements the algorithm used when matching a module path to the
   797  // GOPRIVATE environment variable, as described by 'go help module-private'.
   798  //
   799  // It ignores any empty or malformed patterns in the list.
   800  // Trailing slashes on patterns are ignored.
   801  func MatchPrefixPatterns(globs, target string) bool {
   802  	for globs != "" {
   803  		// Extract next non-empty glob in comma-separated list.
   804  		var glob string
   805  		if i := strings.Index(globs, ","); i >= 0 {
   806  			glob, globs = globs[:i], globs[i+1:]
   807  		} else {
   808  			glob, globs = globs, ""
   809  		}
   810  		glob = strings.TrimSuffix(glob, "/")
   811  		if glob == "" {
   812  			continue
   813  		}
   814  
   815  		// A glob with N+1 path elements (N slashes) needs to be matched
   816  		// against the first N+1 path elements of target,
   817  		// which end just before the N+1'th slash.
   818  		n := strings.Count(glob, "/")
   819  		prefix := target
   820  		// Walk target, counting slashes, truncating at the N+1'th slash.
   821  		for i := 0; i < len(target); i++ {
   822  			if target[i] == '/' {
   823  				if n == 0 {
   824  					prefix = target[:i]
   825  					break
   826  				}
   827  				n--
   828  			}
   829  		}
   830  		if n > 0 {
   831  			// Not enough prefix elements.
   832  			continue
   833  		}
   834  		matched, _ := path.Match(glob, prefix)
   835  		if matched {
   836  			return true
   837  		}
   838  	}
   839  	return false
   840  }
   841  

View as plain text