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 }