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