src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

module.go (27529B)


      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
    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 before, after, ok := strings.Cut(globs, ","); ok {
    806 			glob, globs = before, after
    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 }