...

Source file src/cuelang.org/go/mod/module/module.go

Documentation: cuelang.org/go/mod/module

     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 [Version] type along with support code.
     6  //
     7  // WARNING: THIS PACKAGE IS EXPERIMENTAL.
     8  // ITS API MAY CHANGE AT ANY TIME.
     9  //
    10  // The [Version] type holds a pair of module path and version.
    11  // The module path conforms to the checks implemented by [Check].
    12  //
    13  // # Escaped Paths
    14  //
    15  // Module versions appear as substrings of file system paths (as stored by
    16  // the modcache package).
    17  // In general we cannot rely on file systems to be case-sensitive. Although
    18  // module paths cannot currently contain upper case characters because
    19  // OCI registries forbid that, versions can. That
    20  // is, we cannot rely on the file system to keep foo.com/v@v1.0.0-PRE and
    21  // foo.com/v@v1.0.0-PRE separate. Windows and macOS don't. Instead, we must
    22  // never require two different casings of a file path.
    23  //
    24  // One possibility would be to make the escaped form be the lowercase
    25  // hexadecimal encoding of the actual path bytes. This would avoid ever
    26  // needing different casings of a file path, but it would be fairly illegible
    27  // to most programmers when those paths appeared in the file system
    28  // (including in file paths in compiler errors and stack traces)
    29  // in web server logs, and so on. Instead, we want a safe escaped form that
    30  // leaves most paths unaltered.
    31  //
    32  // The safe escaped form is to replace every uppercase letter
    33  // with an exclamation mark followed by the letter's lowercase equivalent.
    34  //
    35  // For example,
    36  //
    37  //	foo.com/v@v1.0.0-PRE ->  foo.com/v@v1.0.0-!p!r!e
    38  //
    39  // Versions that avoid upper-case letters are left unchanged.
    40  // Note that because import paths are ASCII-only and avoid various
    41  // problematic punctuation (like : < and >), the escaped form is also ASCII-only
    42  // and avoids the same problematic punctuation.
    43  //
    44  // Neither versions nor module paths allow exclamation marks, so there is no
    45  // need to define how to escape a literal !.
    46  //
    47  // # Unicode Restrictions
    48  //
    49  // Today, paths are disallowed from using Unicode.
    50  //
    51  // Although paths are currently disallowed from using Unicode,
    52  // we would like at some point to allow Unicode letters as well, to assume that
    53  // file systems and URLs are Unicode-safe (storing UTF-8), and apply
    54  // the !-for-uppercase convention for escaping them in the file system.
    55  // But there are at least two subtle considerations.
    56  //
    57  // First, note that not all case-fold equivalent distinct runes
    58  // form an upper/lower pair.
    59  // For example, U+004B ('K'), U+006B ('k'), and U+212A ('K' for Kelvin)
    60  // are three distinct runes that case-fold to each other.
    61  // When we do add Unicode letters, we must not assume that upper/lower
    62  // are the only case-equivalent pairs.
    63  // Perhaps the Kelvin symbol would be disallowed entirely, for example.
    64  // Or perhaps it would escape as "!!k", or perhaps as "(212A)".
    65  //
    66  // Second, it would be nice to allow Unicode marks as well as letters,
    67  // but marks include combining marks, and then we must deal not
    68  // only with case folding but also normalization: both U+00E9 ('é')
    69  // and U+0065 U+0301 ('e' followed by combining acute accent)
    70  // look the same on the page and are treated by some file systems
    71  // as the same path. If we do allow Unicode marks in paths, there
    72  // must be some kind of normalization to allow only one canonical
    73  // encoding of any character used in an import path.
    74  package module
    75  
    76  // IMPORTANT NOTE
    77  //
    78  // This file essentially defines the set of valid import paths for the cue command.
    79  // There are many subtle considerations, including Unicode ambiguity,
    80  // security, network, and file system representations.
    81  
    82  import (
    83  	"fmt"
    84  	"sort"
    85  	"strings"
    86  
    87  	"cuelang.org/go/internal/mod/semver"
    88  )
    89  
    90  // A Version (for clients, a module.Version) is defined by a module path and version pair.
    91  // These are stored in their plain (unescaped) form.
    92  // This type is comparable.
    93  type Version struct {
    94  	path    string
    95  	version string
    96  }
    97  
    98  // Path returns the module path part of the Version,
    99  // which always includes the major version suffix
   100  // unless a module path, like "github.com/foo/bar@v0".
   101  // Note that in general the path should include the major version suffix
   102  // even though it's implied from the version. The Canonical
   103  // method can be used to add the major version suffix if not present.
   104  // The BasePath method can be used to obtain the path without
   105  // the suffix.
   106  func (m Version) Path() string {
   107  	return m.path
   108  }
   109  
   110  // Equal reports whether m is equal to m1.
   111  func (m Version) Equal(m1 Version) bool {
   112  	return m.path == m1.path && m.version == m1.version
   113  }
   114  
   115  // BasePath returns the path part of m without its major version suffix.
   116  func (m Version) BasePath() string {
   117  	if m.IsLocal() {
   118  		return m.path
   119  	}
   120  	basePath, _, ok := SplitPathVersion(m.path)
   121  	if !ok {
   122  		panic(fmt.Errorf("broken invariant: failed to split version in %q", m.path))
   123  	}
   124  	return basePath
   125  }
   126  
   127  // Version returns the version part of m. This is either
   128  // a canonical semver version or "none" or the empty string.
   129  func (m Version) Version() string {
   130  	return m.version
   131  }
   132  
   133  // IsValid reports whether m is non-zero.
   134  func (m Version) IsValid() bool {
   135  	return m.path != ""
   136  }
   137  
   138  // IsCanonical reports whether m is valid and has a canonical
   139  // semver version.
   140  func (m Version) IsCanonical() bool {
   141  	return m.IsValid() && m.version != "" && m.version != "none"
   142  }
   143  
   144  func (m Version) IsLocal() bool {
   145  	return m.path == "local"
   146  }
   147  
   148  // String returns the string form of the Version:
   149  // (Path@Version, or just Path if Version is empty).
   150  func (m Version) String() string {
   151  	if m.version == "" {
   152  		return m.path
   153  	}
   154  	return m.BasePath() + "@" + m.version
   155  }
   156  
   157  func MustParseVersion(s string) Version {
   158  	v, err := ParseVersion(s)
   159  	if err != nil {
   160  		panic(err)
   161  	}
   162  	return v
   163  }
   164  
   165  // ParseVersion parses a $module@$version
   166  // string into a Version.
   167  // The version must be canonical (i.e. it can't be
   168  // just a major version).
   169  func ParseVersion(s string) (Version, error) {
   170  	basePath, vers, ok := SplitPathVersion(s)
   171  	if !ok {
   172  		return Version{}, fmt.Errorf("invalid module path@version %q", s)
   173  	}
   174  	if semver.Canonical(vers) != vers {
   175  		return Version{}, fmt.Errorf("module version in %q is not canonical", s)
   176  	}
   177  	return Version{basePath + "@" + semver.Major(vers), vers}, nil
   178  }
   179  
   180  func MustNewVersion(path string, version string) Version {
   181  	v, err := NewVersion(path, version)
   182  	if err != nil {
   183  		panic(err)
   184  	}
   185  	return v
   186  }
   187  
   188  // NewVersion forms a Version from the given path and version.
   189  // The version must be canonical, empty or "none".
   190  // If the path doesn't have a major version suffix, one will be added
   191  // if the version isn't empty; if the version is empty, it's an error.
   192  //
   193  // As a special case, the path "local" is used to mean all packages
   194  // held in the gen, pkg and usr directories.
   195  func NewVersion(path string, version string) (Version, error) {
   196  	switch {
   197  	case path == "local":
   198  		if version != "" {
   199  			return Version{}, fmt.Errorf("module 'local' cannot have version")
   200  		}
   201  	case version != "" && version != "none":
   202  		if !semver.IsValid(version) {
   203  			return Version{}, fmt.Errorf("version %q (of module %q) is not well formed", version, path)
   204  		}
   205  		if semver.Canonical(version) != version {
   206  			return Version{}, fmt.Errorf("version %q (of module %q) is not canonical", version, path)
   207  		}
   208  		maj := semver.Major(version)
   209  		_, vmaj, ok := SplitPathVersion(path)
   210  		if ok && maj != vmaj {
   211  			return Version{}, fmt.Errorf("mismatched major version suffix in %q (version %v)", path, version)
   212  		}
   213  		if !ok {
   214  			fullPath := path + "@" + maj
   215  			if _, _, ok := SplitPathVersion(fullPath); !ok {
   216  				return Version{}, fmt.Errorf("cannot form version path from %q, version %v", path, version)
   217  			}
   218  			path = fullPath
   219  		}
   220  	default:
   221  		base, _, ok := SplitPathVersion(path)
   222  		if !ok {
   223  			return Version{}, fmt.Errorf("path %q has no major version", path)
   224  		}
   225  		if base == "local" {
   226  			return Version{}, fmt.Errorf("module 'local' cannot have version")
   227  		}
   228  	}
   229  	if version == "" {
   230  		if err := CheckPath(path); err != nil {
   231  			return Version{}, err
   232  		}
   233  	} else {
   234  		if err := Check(path, version); err != nil {
   235  			return Version{}, err
   236  		}
   237  	}
   238  	return Version{
   239  		path:    path,
   240  		version: version,
   241  	}, nil
   242  }
   243  
   244  // Sort sorts the list by Path, breaking ties by comparing Version fields.
   245  // The Version fields are interpreted as semantic versions (using semver.Compare)
   246  // optionally followed by a tie-breaking suffix introduced by a slash character,
   247  // like in "v0.0.1/module.cue".
   248  func Sort(list []Version) {
   249  	sort.Slice(list, func(i, j int) bool {
   250  		mi := list[i]
   251  		mj := list[j]
   252  		if mi.path != mj.path {
   253  			return mi.path < mj.path
   254  		}
   255  		// To help go.sum formatting, allow version/file.
   256  		// Compare semver prefix by semver rules,
   257  		// file by string order.
   258  		vi := mi.version
   259  		vj := mj.version
   260  		var fi, fj string
   261  		if k := strings.Index(vi, "/"); k >= 0 {
   262  			vi, fi = vi[:k], vi[k:]
   263  		}
   264  		if k := strings.Index(vj, "/"); k >= 0 {
   265  			vj, fj = vj[:k], vj[k:]
   266  		}
   267  		if vi != vj {
   268  			return semver.Compare(vi, vj) < 0
   269  		}
   270  		return fi < fj
   271  	})
   272  }
   273  

View as plain text