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