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