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