cuelang.org/go@v0.10.1/mod/module/module.go (about)

     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  	"cmp"
    84  	"fmt"
    85  	"slices"
    86  	"strings"
    87  
    88  	"cuelang.org/go/internal/mod/semver"
    89  )
    90  
    91  // A Version (for clients, a module.Version) is defined by a module path and version pair.
    92  // These are stored in their plain (unescaped) form.
    93  // This type is comparable.
    94  type Version struct {
    95  	path    string
    96  	version string
    97  }
    98  
    99  // Path returns the module path part of the Version,
   100  // which always includes the major version suffix
   101  // unless a module path, like "github.com/foo/bar@v0".
   102  // Note that in general the path should include the major version suffix
   103  // even though it's implied from the version. The Canonical
   104  // method can be used to add the major version suffix if not present.
   105  // The BasePath method can be used to obtain the path without
   106  // the suffix.
   107  func (m Version) Path() string {
   108  	return m.path
   109  }
   110  
   111  // Equal reports whether m is equal to m1.
   112  func (m Version) Equal(m1 Version) bool {
   113  	return m.path == m1.path && m.version == m1.version
   114  }
   115  
   116  // BasePath returns the path part of m without its major version suffix.
   117  func (m Version) BasePath() string {
   118  	if m.IsLocal() {
   119  		return m.path
   120  	}
   121  	basePath, _, ok := SplitPathVersion(m.path)
   122  	if !ok {
   123  		panic(fmt.Errorf("broken invariant: failed to split version in %q", m.path))
   124  	}
   125  	return basePath
   126  }
   127  
   128  // Version returns the version part of m. This is either
   129  // a canonical semver version or "none" or the empty string.
   130  func (m Version) Version() string {
   131  	return m.version
   132  }
   133  
   134  // IsValid reports whether m is non-zero.
   135  func (m Version) IsValid() bool {
   136  	return m.path != ""
   137  }
   138  
   139  // IsCanonical reports whether m is valid and has a canonical
   140  // semver version.
   141  func (m Version) IsCanonical() bool {
   142  	return m.IsValid() && m.version != "" && m.version != "none"
   143  }
   144  
   145  func (m Version) IsLocal() bool {
   146  	return m.path == "local"
   147  }
   148  
   149  // String returns the string form of the Version:
   150  // (Path@Version, or just Path if Version is empty).
   151  func (m Version) String() string {
   152  	if m.version == "" {
   153  		return m.path
   154  	}
   155  	return m.BasePath() + "@" + m.version
   156  }
   157  
   158  func MustParseVersion(s string) Version {
   159  	v, err := ParseVersion(s)
   160  	if err != nil {
   161  		panic(err)
   162  	}
   163  	return v
   164  }
   165  
   166  // ParseVersion parses a $module@$version
   167  // string into a Version.
   168  // The version must be canonical (i.e. it can't be
   169  // just a major version).
   170  func ParseVersion(s string) (Version, error) {
   171  	basePath, vers, ok := SplitPathVersion(s)
   172  	if !ok {
   173  		return Version{}, fmt.Errorf("invalid module path@version %q", s)
   174  	}
   175  	if semver.Canonical(vers) != vers {
   176  		return Version{}, fmt.Errorf("module version in %q is not canonical", s)
   177  	}
   178  	return Version{basePath + "@" + semver.Major(vers), vers}, nil
   179  }
   180  
   181  func MustNewVersion(path string, version string) Version {
   182  	v, err := NewVersion(path, version)
   183  	if err != nil {
   184  		panic(err)
   185  	}
   186  	return v
   187  }
   188  
   189  // NewVersion forms a Version from the given path and version.
   190  // The version must be canonical, empty or "none".
   191  // If the path doesn't have a major version suffix, one will be added
   192  // if the version isn't empty; if the version is empty, it's an error.
   193  //
   194  // As a special case, the path "local" is used to mean all packages
   195  // held in the gen, pkg and usr directories.
   196  func NewVersion(path string, version string) (Version, error) {
   197  	switch {
   198  	case path == "local":
   199  		if version != "" {
   200  			return Version{}, fmt.Errorf("module 'local' cannot have version")
   201  		}
   202  	case version != "" && version != "none":
   203  		if !semver.IsValid(version) {
   204  			return Version{}, fmt.Errorf("version %q (of module %q) is not well formed", version, path)
   205  		}
   206  		if semver.Canonical(version) != version {
   207  			return Version{}, fmt.Errorf("version %q (of module %q) is not canonical", version, path)
   208  		}
   209  		maj := semver.Major(version)
   210  		_, vmaj, ok := SplitPathVersion(path)
   211  		if ok && maj != vmaj {
   212  			return Version{}, fmt.Errorf("mismatched major version suffix in %q (version %v)", path, version)
   213  		}
   214  		if !ok {
   215  			fullPath := path + "@" + maj
   216  			if _, _, ok := SplitPathVersion(fullPath); !ok {
   217  				return Version{}, fmt.Errorf("cannot form version path from %q, version %v", path, version)
   218  			}
   219  			path = fullPath
   220  		}
   221  	default:
   222  		base, _, ok := SplitPathVersion(path)
   223  		if !ok {
   224  			return Version{}, fmt.Errorf("path %q has no major version", path)
   225  		}
   226  		if base == "local" {
   227  			return Version{}, fmt.Errorf("module 'local' cannot have version")
   228  		}
   229  	}
   230  	if version == "" {
   231  		if err := CheckPath(path); err != nil {
   232  			return Version{}, err
   233  		}
   234  	} else {
   235  		if err := Check(path, version); err != nil {
   236  			return Version{}, err
   237  		}
   238  	}
   239  	return Version{
   240  		path:    path,
   241  		version: version,
   242  	}, nil
   243  }
   244  
   245  // Sort sorts the list by Path, breaking ties by comparing Version fields.
   246  // The Version fields are interpreted as semantic versions (using semver.Compare)
   247  // optionally followed by a tie-breaking suffix introduced by a slash character,
   248  // like in "v0.0.1/module.cue".
   249  func Sort(list []Version) {
   250  	slices.SortFunc(list, func(a, b Version) int {
   251  		if c := cmp.Compare(a.path, b.path); c != 0 {
   252  			return c
   253  		}
   254  		// To help go.sum formatting, allow version/file.
   255  		// Compare semver prefix by semver rules,
   256  		// file by string order.
   257  		va := a.version
   258  		vb := b.version
   259  		var fa, fb string
   260  		if k := strings.Index(va, "/"); k >= 0 {
   261  			va, fa = va[:k], va[k:]
   262  		}
   263  		if k := strings.Index(vb, "/"); k >= 0 {
   264  			vb, fb = vb[:k], vb[k:]
   265  		}
   266  		if c := semver.Compare(va, vb); c != 0 {
   267  			return c
   268  		}
   269  		return cmp.Compare(fa, fb)
   270  	})
   271  }