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