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.
5 // Package module defines the module.Version type along with support code.
7 // The module.Version type is a simple Path, Version pair:
9 // type Version struct {
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.
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.
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.
41 // The safe escaped form is to replace every uppercase letter
42 // with an exclamation mark followed by the letter's lowercase equivalent.
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.
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.
55 // Import paths have never allowed exclamation marks, so there is no
56 // need to define how to escape a literal !.
58 // Unicode Restrictions
60 // Today, paths are disallowed from using Unicode.
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.
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)".
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.
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.
93 // This file also defines the set of valid module path and version combinations,
94 // another topic with many subtle considerations.
96 // Changes to the semantics in this file require approval from rsc.
105 "golang.org/x/mod/semver"
106 errors "golang.org/x/xerrors"
109 // A Version (for clients, a module.Version) is defined by a module path and version pair.
110 // These are stored in their plain (unescaped) form.
111 type Version struct {
112 // Path is a module path, like "golang.org/x/text" or "rsc.io/quote/v2".
115 // Version is usually a semantic version in canonical form.
116 // There are three exceptions to this general rule.
117 // First, the top-level target of a build has no specific version
118 // and uses Version = "".
119 // Second, during MVS calculations the version "none" is used
120 // to represent the decision to take no version of a given module.
121 // Third, filesystem paths found in "replace" directives are
122 // represented by a path with an empty version.
123 Version string `json:",omitempty"`
126 // String returns a representation of the Version suitable for logging
127 // (Path@Version, or just Path if Version is empty).
128 func (m Version) String() string {
132 return m.Path + "@" + m.Version
135 // A ModuleError indicates an error specific to a module.
136 type ModuleError struct {
142 // VersionError returns a ModuleError derived from a Version and error,
143 // or err itself if it is already such an error.
144 func VersionError(v Version, err error) error {
145 var mErr *ModuleError
146 if errors.As(err, &mErr) && mErr.Path == v.Path && mErr.Version == v.Version {
156 func (e *ModuleError) Error() string {
157 if v, ok := e.Err.(*InvalidVersionError); ok {
158 return fmt.Sprintf("%s@%s: invalid %s: %v", e.Path, v.Version, v.noun(), v.Err)
161 return fmt.Sprintf("%s@%s: %v", e.Path, e.Version, e.Err)
163 return fmt.Sprintf("module %s: %v", e.Path, e.Err)
166 func (e *ModuleError) Unwrap() error { return e.Err }
168 // An InvalidVersionError indicates an error specific to a version, with the
169 // module path unknown or specified externally.
171 // A ModuleError may wrap an InvalidVersionError, but an InvalidVersionError
172 // must not wrap a ModuleError.
173 type InvalidVersionError struct {
179 // noun returns either "version" or "pseudo-version", depending on whether
180 // e.Version is a pseudo-version.
181 func (e *InvalidVersionError) noun() string {
183 return "pseudo-version"
188 func (e *InvalidVersionError) Error() string {
189 return fmt.Sprintf("%s %q invalid: %s", e.noun(), e.Version, e.Err)
192 func (e *InvalidVersionError) Unwrap() error { return e.Err }
194 // Check checks that a given module path, version pair is valid.
195 // In addition to the path being a valid module path
196 // and the version being a valid semantic version,
197 // the two must correspond.
198 // For example, the path "yaml/v2" only corresponds to
199 // semantic versions beginning with "v2.".
200 func Check(path, version string) error {
201 if err := CheckPath(path); err != nil {
204 if !semver.IsValid(version) {
207 Err: &InvalidVersionError{Version: version, Err: errors.New("not a semantic version")},
210 _, pathMajor, _ := SplitPathVersion(path)
211 if err := CheckPathMajor(version, pathMajor); err != nil {
212 return &ModuleError{Path: path, Err: err}
217 // firstPathOK reports whether r can appear in the first element of a module path.
218 // The first element of the path must be an LDH domain name, at least for now.
219 // To avoid case ambiguity, the domain name must be entirely lower case.
220 func firstPathOK(r rune) bool {
221 return r == '-' || r == '.' ||
222 '0' <= r && r <= '9' ||
226 // pathOK reports whether r can appear in an import path element.
227 // Paths can be ASCII letters, ASCII digits, and limited ASCII punctuation: + - . _ and ~.
228 // This matches what "go get" has historically recognized in import paths.
229 // TODO(rsc): We would like to allow Unicode letters, but that requires additional
230 // care in the safe encoding (see "escaped paths" above).
231 func pathOK(r rune) bool {
232 if r < utf8.RuneSelf {
233 return r == '+' || r == '-' || r == '.' || r == '_' || r == '~' ||
234 '0' <= r && r <= '9' ||
235 'A' <= r && r <= 'Z' ||
241 // fileNameOK reports whether r can appear in a file name.
242 // For now we allow all Unicode letters but otherwise limit to pathOK plus a few more punctuation characters.
243 // If we expand the set of allowed characters here, we have to
244 // work harder at detecting potential case-folding and normalization collisions.
245 // See note about "escaped paths" above.
246 func fileNameOK(r rune) bool {
247 if r < utf8.RuneSelf {
248 // Entire set of ASCII punctuation, from which we remove characters:
249 // ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~
250 // We disallow some shell special characters: " ' * < > ? ` |
251 // (Note that some of those are disallowed by the Windows file system as well.)
252 // We also disallow path separators / : and \ (fileNameOK is only called on path element characters).
253 // We allow spaces (U+0020) in file names.
254 const allowed = "!#$%&()+,-.=@[]^_{}~ "
255 if '0' <= r && r <= '9' || 'A' <= r && r <= 'Z' || 'a' <= r && r <= 'z' {
258 for i := 0; i < len(allowed); i++ {
259 if rune(allowed[i]) == r {
265 // It may be OK to add more ASCII punctuation here, but only carefully.
266 // For example Windows disallows < > \, and macOS disallows :, so we must not allow those.
267 return unicode.IsLetter(r)
270 // CheckPath checks that a module path is valid.
271 // A valid module path is a valid import path, as checked by CheckImportPath,
272 // with two additional constraints.
273 // First, the leading path element (up to the first slash, if any),
274 // by convention a domain name, must contain only lower-case ASCII letters,
275 // ASCII digits, dots (U+002E), and dashes (U+002D);
276 // it must contain at least one dot and cannot start with a dash.
277 // Second, for a final path element of the form /vN, where N looks numeric
278 // (ASCII digits and dots) must not begin with a leading zero, must not be /v1,
279 // and must not contain any dots. For paths beginning with "gopkg.in/",
280 // this second requirement is replaced by a requirement that the path
281 // follow the gopkg.in server's conventions.
282 func CheckPath(path string) error {
283 if err := checkPath(path, false); err != nil {
284 return fmt.Errorf("malformed module path %q: %v", path, err)
286 i := strings.Index(path, "/")
291 return fmt.Errorf("malformed module path %q: leading slash", path)
293 if !strings.Contains(path[:i], ".") {
294 return fmt.Errorf("malformed module path %q: missing dot in first path element", path)
297 return fmt.Errorf("malformed module path %q: leading dash in first path element", path)
299 for _, r := range path[:i] {
301 return fmt.Errorf("malformed module path %q: invalid char %q in first path element", path, r)
304 if _, _, ok := SplitPathVersion(path); !ok {
305 return fmt.Errorf("malformed module path %q: invalid version", path)
310 // CheckImportPath checks that an import path is valid.
312 // A valid import path consists of one or more valid path elements
313 // separated by slashes (U+002F). (It must not begin with nor end in a slash.)
315 // A valid path element is a non-empty string made up of
316 // ASCII letters, ASCII digits, and limited ASCII punctuation: + - . _ and ~.
317 // It must not begin or end with a dot (U+002E), nor contain two dots in a row.
319 // The element prefix up to the first dot must not be a reserved file name
320 // on Windows, regardless of case (CON, com1, NuL, and so on).
322 // CheckImportPath may be less restrictive in the future, but see the
323 // top-level package documentation for additional information about
324 // subtleties of Unicode.
325 func CheckImportPath(path string) error {
326 if err := checkPath(path, false); err != nil {
327 return fmt.Errorf("malformed import path %q: %v", path, err)
332 // checkPath checks that a general path is valid.
333 // It returns an error describing why but not mentioning path.
334 // Because these checks apply to both module paths and import paths,
335 // the caller is expected to add the "malformed ___ path %q: " prefix.
336 // fileName indicates whether the final element of the path is a file name
337 // (as opposed to a directory name).
338 func checkPath(path string, fileName bool) error {
339 if !utf8.ValidString(path) {
340 return fmt.Errorf("invalid UTF-8")
343 return fmt.Errorf("empty string")
346 return fmt.Errorf("leading dash")
348 if strings.Contains(path, "//") {
349 return fmt.Errorf("double slash")
351 if path[len(path)-1] == '/' {
352 return fmt.Errorf("trailing slash")
355 for i, r := range path {
357 if err := checkElem(path[elemStart:i], fileName); err != nil {
363 if err := checkElem(path[elemStart:], fileName); err != nil {
369 // checkElem checks whether an individual path element is valid.
370 // fileName indicates whether the element is a file name (not a directory name).
371 func checkElem(elem string, fileName bool) error {
373 return fmt.Errorf("empty path element")
375 if strings.Count(elem, ".") == len(elem) {
376 return fmt.Errorf("invalid path element %q", elem)
378 if elem[0] == '.' && !fileName {
379 return fmt.Errorf("leading dot in path element")
381 if elem[len(elem)-1] == '.' {
382 return fmt.Errorf("trailing dot in path element")
388 for _, r := range elem {
390 return fmt.Errorf("invalid char %q", r)
394 // Windows disallows a bunch of path elements, sadly.
395 // See https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
397 if i := strings.Index(short, "."); i >= 0 {
400 for _, bad := range badWindowsNames {
401 if strings.EqualFold(bad, short) {
402 return fmt.Errorf("%q disallowed as path element component on Windows", short)
408 // CheckFilePath checks that a slash-separated file path is valid.
409 // The definition of a valid file path is the same as the definition
410 // of a valid import path except that the set of allowed characters is larger:
411 // all Unicode letters, ASCII digits, the ASCII space character (U+0020),
412 // and the ASCII punctuation characters
413 // “!#$%&()+,-.=@[]^_{}~”.
414 // (The excluded punctuation characters, " * < > ? ` ' | / \ and :,
415 // have special meanings in certain shells or operating systems.)
417 // CheckFilePath may be less restrictive in the future, but see the
418 // top-level package documentation for additional information about
419 // subtleties of Unicode.
420 func CheckFilePath(path string) error {
421 if err := checkPath(path, true); err != nil {
422 return fmt.Errorf("malformed file path %q: %v", path, err)
427 // badWindowsNames are the reserved file path elements on Windows.
428 // See https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
429 var badWindowsNames = []string{
454 // SplitPathVersion returns prefix and major version such that prefix+pathMajor == path
455 // and version is either empty or "/vN" for N >= 2.
456 // As a special case, gopkg.in paths are recognized directly;
457 // they require ".vN" instead of "/vN", and for all N, not just N >= 2.
458 // SplitPathVersion returns with ok = false when presented with
459 // a path whose last path element does not satisfy the constraints
460 // applied by CheckPath, such as "example.com/pkg/v1" or "example.com/pkg/v1.2".
461 func SplitPathVersion(path string) (prefix, pathMajor string, ok bool) {
462 if strings.HasPrefix(path, "gopkg.in/") {
463 return splitGopkgIn(path)
468 for i > 0 && ('0' <= path[i-1] && path[i-1] <= '9' || path[i-1] == '.') {
469 if path[i-1] == '.' {
474 if i <= 1 || i == len(path) || path[i-1] != 'v' || path[i-2] != '/' {
475 return path, "", true
477 prefix, pathMajor = path[:i-2], path[i-2:]
478 if dot || len(pathMajor) <= 2 || pathMajor[2] == '0' || pathMajor == "/v1" {
479 return path, "", false
481 return prefix, pathMajor, true
484 // splitGopkgIn is like SplitPathVersion but only for gopkg.in paths.
485 func splitGopkgIn(path string) (prefix, pathMajor string, ok bool) {
486 if !strings.HasPrefix(path, "gopkg.in/") {
487 return path, "", false
490 if strings.HasSuffix(path, "-unstable") {
491 i -= len("-unstable")
493 for i > 0 && ('0' <= path[i-1] && path[i-1] <= '9') {
496 if i <= 1 || path[i-1] != 'v' || path[i-2] != '.' {
497 // All gopkg.in paths must end in vN for some N.
498 return path, "", false
500 prefix, pathMajor = path[:i-2], path[i-2:]
501 if len(pathMajor) <= 2 || pathMajor[2] == '0' && pathMajor != ".v0" {
502 return path, "", false
504 return prefix, pathMajor, true
507 // MatchPathMajor reports whether the semantic version v
508 // matches the path major version pathMajor.
510 // MatchPathMajor returns true if and only if CheckPathMajor returns nil.
511 func MatchPathMajor(v, pathMajor string) bool {
512 return CheckPathMajor(v, pathMajor) == nil
515 // CheckPathMajor returns a non-nil error if the semantic version v
516 // does not match the path major version pathMajor.
517 func CheckPathMajor(v, pathMajor string) error {
518 // TODO(jayconrod): return errors or panic for invalid inputs. This function
519 // (and others) was covered by integration tests for cmd/go, and surrounding
520 // code protected against invalid inputs like non-canonical versions.
521 if strings.HasPrefix(pathMajor, ".v") && strings.HasSuffix(pathMajor, "-unstable") {
522 pathMajor = strings.TrimSuffix(pathMajor, "-unstable")
524 if strings.HasPrefix(v, "v0.0.0-") && pathMajor == ".v1" {
525 // Allow old bug in pseudo-versions that generated v0.0.0- pseudoversion for gopkg .v1.
526 // For example, gopkg.in/yaml.v2@v2.2.1's go.mod requires gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405.
531 if m == "v0" || m == "v1" || semver.Build(v) == "+incompatible" {
534 pathMajor = "v0 or v1"
535 } else if pathMajor[0] == '/' || pathMajor[0] == '.' {
536 if m == pathMajor[1:] {
539 pathMajor = pathMajor[1:]
541 return &InvalidVersionError{
543 Err: fmt.Errorf("should be %s, not %s", pathMajor, semver.Major(v)),
547 // PathMajorPrefix returns the major-version tag prefix implied by pathMajor.
548 // An empty PathMajorPrefix allows either v0 or v1.
550 // Note that MatchPathMajor may accept some versions that do not actually begin
551 // with this prefix: namely, it accepts a 'v0.0.0-' prefix for a '.v1'
552 // pathMajor, even though that pathMajor implies 'v1' tagging.
553 func PathMajorPrefix(pathMajor string) string {
557 if pathMajor[0] != '/' && pathMajor[0] != '.' {
558 panic("pathMajor suffix " + pathMajor + " passed to PathMajorPrefix lacks separator")
560 if strings.HasPrefix(pathMajor, ".v") && strings.HasSuffix(pathMajor, "-unstable") {
561 pathMajor = strings.TrimSuffix(pathMajor, "-unstable")
564 if m != semver.Major(m) {
565 panic("pathMajor suffix " + pathMajor + "passed to PathMajorPrefix is not a valid major version")
570 // CanonicalVersion returns the canonical form of the version string v.
571 // It is the same as semver.Canonical(v) except that it preserves the special build suffix "+incompatible".
572 func CanonicalVersion(v string) string {
573 cv := semver.Canonical(v)
574 if semver.Build(v) == "+incompatible" {
575 cv += "+incompatible"
580 // Sort sorts the list by Path, breaking ties by comparing Version fields.
581 // The Version fields are interpreted as semantic versions (using semver.Compare)
582 // optionally followed by a tie-breaking suffix introduced by a slash character,
583 // like in "v0.0.1/go.mod".
584 func Sort(list []Version) {
585 sort.Slice(list, func(i, j int) bool {
588 if mi.Path != mj.Path {
589 return mi.Path < mj.Path
591 // To help go.sum formatting, allow version/file.
592 // Compare semver prefix by semver rules,
593 // file by string order.
597 if k := strings.Index(vi, "/"); k >= 0 {
598 vi, fi = vi[:k], vi[k:]
600 if k := strings.Index(vj, "/"); k >= 0 {
601 vj, fj = vj[:k], vj[k:]
604 return semver.Compare(vi, vj) < 0
610 // EscapePath returns the escaped form of the given module path.
611 // It fails if the module path is invalid.
612 func EscapePath(path string) (escaped string, err error) {
613 if err := CheckPath(path); err != nil {
617 return escapeString(path)
620 // EscapeVersion returns the escaped form of the given module version.
621 // Versions are allowed to be in non-semver form but must be valid file names
622 // and not contain exclamation marks.
623 func EscapeVersion(v string) (escaped string, err error) {
624 if err := checkElem(v, true); err != nil || strings.Contains(v, "!") {
625 return "", &InvalidVersionError{
627 Err: fmt.Errorf("disallowed version string"),
630 return escapeString(v)
633 func escapeString(s string) (escaped string, err error) {
635 for _, r := range s {
636 if r == '!' || r >= utf8.RuneSelf {
637 // This should be disallowed by CheckPath, but diagnose anyway.
638 // The correctness of the escaping loop below depends on it.
639 return "", fmt.Errorf("internal error: inconsistency in EscapePath")
641 if 'A' <= r && r <= 'Z' {
651 for _, r := range s {
652 if 'A' <= r && r <= 'Z' {
653 buf = append(buf, '!', byte(r+'a'-'A'))
655 buf = append(buf, byte(r))
658 return string(buf), nil
661 // UnescapePath returns the module path for the given escaped path.
662 // It fails if the escaped path is invalid or describes an invalid path.
663 func UnescapePath(escaped string) (path string, err error) {
664 path, ok := unescapeString(escaped)
666 return "", fmt.Errorf("invalid escaped module path %q", escaped)
668 if err := CheckPath(path); err != nil {
669 return "", fmt.Errorf("invalid escaped module path %q: %v", escaped, err)
674 // UnescapeVersion returns the version string for the given escaped version.
675 // It fails if the escaped form is invalid or describes an invalid version.
676 // Versions are allowed to be in non-semver form but must be valid file names
677 // and not contain exclamation marks.
678 func UnescapeVersion(escaped string) (v string, err error) {
679 v, ok := unescapeString(escaped)
681 return "", fmt.Errorf("invalid escaped version %q", escaped)
683 if err := checkElem(v, true); err != nil {
684 return "", fmt.Errorf("invalid escaped version %q: %v", v, err)
689 func unescapeString(escaped string) (string, bool) {
693 for _, r := range escaped {
694 if r >= utf8.RuneSelf {
699 if r < 'a' || 'z' < r {
702 buf = append(buf, byte(r+'A'-'a'))
709 if 'A' <= r && r <= 'Z' {
712 buf = append(buf, byte(r))
717 return string(buf), true