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 }