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