podman

Форк
0
841 строка · 26.9 Кб
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
	"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.
112
type 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).
129
func (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.
137
type 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.
145
func 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

157
func (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

167
func (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.
174
type 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.
182
func (e *InvalidVersionError) noun() string {
183
	if e.Pseudo {
184
		return "pseudo-version"
185
	}
186
	return "version"
187
}
188

189
func (e *InvalidVersionError) Error() string {
190
	return fmt.Sprintf("%s %q invalid: %s", e.noun(), e.Version, e.Err)
191
}
192

193
func (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.
198
type InvalidPathError struct {
199
	Kind string // "module", "import", or "file"
200
	Path string
201
	Err  error
202
}
203

204
func (e *InvalidPathError) Error() string {
205
	return fmt.Sprintf("malformed %s path %q: %v", e.Kind, e.Path, e.Err)
206
}
207

208
func (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.".
216
func 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.
236
func 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).
251
func 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).
268
func 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.
277
func 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.
309
func 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.
360
func 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.
369
type pathKind int
370

371
const (
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].
384
func 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.
416
func 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.
495
func 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
504
var 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".
536
func 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.
560
func 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.
586
func 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.
592
func 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.
628
func 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".
647
func 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".
659
func 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.
687
func 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.
698
func 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

708
func 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.
738
func 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.
753
func 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

764
func 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.
802
func 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

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.