Skip to content

Commit 38cf584

Browse files
committed
internal/mod/module: adapt to CUE semantics
Summary of changes: - module.Version becomes an opaque type so we can be sure that we always obey its rules. The Go code often plays fast and loose with this type, and we have enough associated logic (e.g. major version inference) that it seems worth being more sure. - escaped paths are no longer a thing, because we don't allow upper case in module names - due to the OCI repository name restriction that likewise disallows upper case. - prefix matching isn't needed - gopkg.in isn't a thing in CUE land - splitting path and version is much simpler when the version is always at the end. - pseudo-versions aren't a thing when versions are always explicitly tagged in the registry. Note: experimental feature. For #2330. Signed-off-by: Roger Peppe <[email protected]> Change-Id: Ic4fd59f0992ad53897438d3af796b78ef337b39f Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1168704 Unity-Result: CUE porcuepine <[email protected]> TryBot-Result: CUEcueckoo <[email protected]> Reviewed-by: Paul Jolly <[email protected]>
1 parent 204665c commit 38cf584

File tree

6 files changed

+281
-1174
lines changed

6 files changed

+281
-1174
lines changed

internal/mod/module/error.go

+5-21
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
//go:build ignore
2-
3-
// Copyright 2018 The Go Authors. All rights reserved.
4-
// Use of this source code is governed by a BSD-style
5-
// license that can be found in the LICENSE file.
6-
71
package module
82

93
import (
@@ -22,19 +16,19 @@ type ModuleError struct {
2216
// or err itself if it is already such an error.
2317
func VersionError(v Version, err error) error {
2418
var mErr *ModuleError
25-
if errors.As(err, &mErr) && mErr.Path == v.Path && mErr.Version == v.Version {
19+
if errors.As(err, &mErr) && mErr.Path == v.Path() && mErr.Version == v.Version() {
2620
return err
2721
}
2822
return &ModuleError{
29-
Path: v.Path,
30-
Version: v.Version,
23+
Path: v.Path(),
24+
Version: v.Version(),
3125
Err: err,
3226
}
3327
}
3428

3529
func (e *ModuleError) Error() string {
3630
if v, ok := e.Err.(*InvalidVersionError); ok {
37-
return fmt.Sprintf("%s@%s: invalid %s: %v", e.Path, v.Version, v.noun(), v.Err)
31+
return fmt.Sprintf("%s@%s: invalid version: %v", e.Path, v.Version, v.Err)
3832
}
3933
if e.Version != "" {
4034
return fmt.Sprintf("%s@%s: %v", e.Path, e.Version, e.Err)
@@ -51,21 +45,11 @@ func (e *ModuleError) Unwrap() error { return e.Err }
5145
// must not wrap a ModuleError.
5246
type InvalidVersionError struct {
5347
Version string
54-
Pseudo bool
5548
Err error
5649
}
5750

58-
// noun returns either "version" or "pseudo-version", depending on whether
59-
// e.Version is a pseudo-version.
60-
func (e *InvalidVersionError) noun() string {
61-
if e.Pseudo {
62-
return "pseudo-version"
63-
}
64-
return "version"
65-
}
66-
6751
func (e *InvalidVersionError) Error() string {
68-
return fmt.Sprintf("%s %q invalid: %s", e.noun(), e.Version, e.Err)
52+
return fmt.Sprintf("version %q invalid: %s", e.Version, e.Err)
6953
}
7054

7155
func (e *InvalidVersionError) Unwrap() error { return e.Err }

internal/mod/module/module.go

+106-103
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
//go:build ignore
2-
31
// Copyright 2018 The Go Authors. All rights reserved.
42
// Use of this source code is governed by a BSD-style
53
// license that can be found in the LICENSE file.
@@ -16,88 +14,16 @@
1614
// There are no restrictions imposed directly by use of this structure,
1715
// but additional checking functions, most notably Check, verify that
1816
// a particular path, version pair is valid.
19-
//
20-
// # Escaped Paths
21-
//
22-
// Module paths appear as substrings of file system paths
23-
// (in the download cache) and of web server URLs in the proxy protocol.
24-
// In general we cannot rely on file systems to be case-sensitive,
25-
// nor can we rely on web servers, since they read from file systems.
26-
// That is, we cannot rely on the file system to keep rsc.io/QUOTE
27-
// and rsc.io/quote separate. Windows and macOS don't.
28-
// Instead, we must never require two different casings of a file path.
29-
// Because we want the download cache to match the proxy protocol,
30-
// and because we want the proxy protocol to be possible to serve
31-
// from a tree of static files (which might be stored on a case-insensitive
32-
// file system), the proxy protocol must never require two different casings
33-
// of a URL path either.
34-
//
35-
// One possibility would be to make the escaped form be the lowercase
36-
// hexadecimal encoding of the actual path bytes. This would avoid ever
37-
// needing different casings of a file path, but it would be fairly illegible
38-
// to most programmers when those paths appeared in the file system
39-
// (including in file paths in compiler errors and stack traces)
40-
// in web server logs, and so on. Instead, we want a safe escaped form that
41-
// leaves most paths unaltered.
42-
//
43-
// The safe escaped form is to replace every uppercase letter
44-
// with an exclamation mark followed by the letter's lowercase equivalent.
45-
//
46-
// For example,
47-
//
48-
// github.com/Azure/azure-sdk-for-go -> github.com/!azure/azure-sdk-for-go.
49-
// github.com/GoogleCloudPlatform/cloudsql-proxy -> github.com/!google!cloud!platform/cloudsql-proxy
50-
// github.com/Sirupsen/logrus -> github.com/!sirupsen/logrus.
51-
//
52-
// Import paths that avoid upper-case letters are left unchanged.
53-
// Note that because import paths are ASCII-only and avoid various
54-
// problematic punctuation (like : < and >), the escaped form is also ASCII-only
55-
// and avoids the same problematic punctuation.
56-
//
57-
// Import paths have never allowed exclamation marks, so there is no
58-
// need to define how to escape a literal !.
59-
//
60-
// # Unicode Restrictions
61-
//
62-
// Today, paths are disallowed from using Unicode.
63-
//
64-
// Although paths are currently disallowed from using Unicode,
65-
// we would like at some point to allow Unicode letters as well, to assume that
66-
// file systems and URLs are Unicode-safe (storing UTF-8), and apply
67-
// the !-for-uppercase convention for escaping them in the file system.
68-
// But there are at least two subtle considerations.
69-
//
70-
// First, note that not all case-fold equivalent distinct runes
71-
// form an upper/lower pair.
72-
// For example, U+004B ('K'), U+006B ('k'), and U+212A ('K' for Kelvin)
73-
// are three distinct runes that case-fold to each other.
74-
// When we do add Unicode letters, we must not assume that upper/lower
75-
// are the only case-equivalent pairs.
76-
// Perhaps the Kelvin symbol would be disallowed entirely, for example.
77-
// Or perhaps it would escape as "!!k", or perhaps as "(212A)".
78-
//
79-
// Second, it would be nice to allow Unicode marks as well as letters,
80-
// but marks include combining marks, and then we must deal not
81-
// only with case folding but also normalization: both U+00E9 ('é')
82-
// and U+0065 U+0301 ('e' followed by combining acute accent)
83-
// look the same on the page and are treated by some file systems
84-
// as the same path. If we do allow Unicode marks in paths, there
85-
// must be some kind of normalization to allow only one canonical
86-
// encoding of any character used in an import path.
8717
package module
8818

8919
// IMPORTANT NOTE
9020
//
91-
// This file essentially defines the set of valid import paths for the go command.
21+
// This file essentially defines the set of valid import paths for the cue command.
9222
// There are many subtle considerations, including Unicode ambiguity,
9323
// security, network, and file system representations.
94-
//
95-
// This file also defines the set of valid module path and version combinations,
96-
// another topic with many subtle considerations.
97-
//
98-
// Changes to the semantics in this file require approval from rsc.
9924

10025
import (
26+
"fmt"
10127
"sort"
10228
"strings"
10329

@@ -106,56 +32,133 @@ import (
10632

10733
// A Version (for clients, a module.Version) is defined by a module path and version pair.
10834
// These are stored in their plain (unescaped) form.
35+
// This type is comparable.
10936
type Version struct {
110-
// Path is a module path, like "golang.org/x/text" or "rsc.io/quote/v2".
111-
Path string
37+
path string
38+
version string
39+
}
11240

113-
// Version is usually a semantic version in canonical form.
114-
// There are three exceptions to this general rule.
115-
// First, the top-level target of a build has no specific version
116-
// and uses Version = "".
117-
// Second, during MVS calculations the version "none" is used
118-
// to represent the decision to take no version of a given module.
119-
// Third, filesystem paths found in "replace" directives are
120-
// represented by a path with an empty version.
121-
Version string `json:",omitempty"`
41+
// Path returns the module path part of the Version,
42+
// which always includes the major version suffix
43+
// unless a module path, like "github.com/foo/bar@v0".
44+
// Note that in general the path should include the major version suffix
45+
// even though it's implied from the version. The Canonical
46+
// method can be used to add the major version suffix if not present.
47+
// The BasePath method can be used to obtain the path without
48+
// the suffix.
49+
func (m Version) Path() string {
50+
return m.path
51+
}
52+
53+
func (m Version) BasePath() string {
54+
basePath, _, ok := SplitPathVersion(m.path)
55+
if !ok {
56+
panic(fmt.Errorf("broken invariant: failed to split version in %q", m.path))
57+
}
58+
return basePath
12259
}
12360

124-
// String returns a representation of the Version suitable for logging
61+
func (m Version) Version() string {
62+
return m.version
63+
}
64+
65+
// String returns the string form of the Version:
12566
// (Path@Version, or just Path if Version is empty).
12667
func (m Version) String() string {
127-
if m.Version == "" {
128-
return m.Path
68+
if m.version == "" {
69+
return m.path
70+
}
71+
return m.BasePath() + "@" + m.version
72+
}
73+
74+
func MustParseVersion(s string) Version {
75+
v, err := ParseVersion(s)
76+
if err != nil {
77+
panic(err)
12978
}
130-
return m.Path + "@" + m.Version
79+
return v
13180
}
13281

133-
// CanonicalVersion returns the canonical form of the version string v.
134-
// It is the same as semver.Canonical(v) except that it preserves the special build suffix "+incompatible".
135-
func CanonicalVersion(v string) string {
136-
cv := semver.Canonical(v)
137-
if semver.Build(v) == "+incompatible" {
138-
cv += "+incompatible"
82+
// ParseVersion parses a $module@$version
83+
// string into a Version.
84+
// The version must be canonical (i.e. it can't be
85+
// just a major version).
86+
func ParseVersion(s string) (Version, error) {
87+
basePath, vers, ok := SplitPathVersion(s)
88+
if !ok {
89+
return Version{}, fmt.Errorf("invalid module path@version %q", s)
90+
}
91+
if semver.Canonical(vers) != vers {
92+
return Version{}, fmt.Errorf("module version in %q is not canonical", s)
93+
}
94+
return Version{basePath + "@" + semver.Major(vers), vers}, nil
95+
}
96+
97+
func MustNewVersion(path string, vers string) Version {
98+
v, err := NewVersion(path, vers)
99+
if err != nil {
100+
panic(err)
101+
}
102+
return v
103+
}
104+
105+
// NewVersion forms a Version from the given path and version.
106+
// The version must be canonical or empty.
107+
// If the path doesn't have a major version suffix, one will be added
108+
// if the version isn't empty; if the version is empty, it's an error.
109+
func NewVersion(path string, vers string) (Version, error) {
110+
if vers != "" && vers != "none" {
111+
if semver.Canonical(vers) != vers {
112+
return Version{}, fmt.Errorf("version %q (of module %q) is not canonical", vers, path)
113+
}
114+
maj := semver.Major(vers)
115+
_, vmaj, ok := SplitPathVersion(path)
116+
if ok && maj != vmaj {
117+
return Version{}, fmt.Errorf("mismatched major version suffix in %q (version %v)", path, vers)
118+
}
119+
if !ok {
120+
fullPath := path + "@" + maj
121+
if _, _, ok := SplitPathVersion(fullPath); !ok {
122+
return Version{}, fmt.Errorf("cannot form version path from %q, version %v", path, vers)
123+
}
124+
path = fullPath
125+
}
126+
} else {
127+
if _, _, ok := SplitPathVersion(path); !ok {
128+
return Version{}, fmt.Errorf("path %q has no major version", path)
129+
}
130+
}
131+
if vers == "" {
132+
if err := CheckPath(path); err != nil {
133+
return Version{}, err
134+
}
135+
} else {
136+
if err := Check(path, vers); err != nil {
137+
return Version{}, err
138+
}
139139
}
140-
return cv
140+
return Version{
141+
path: path,
142+
version: vers,
143+
}, nil
141144
}
142145

143146
// Sort sorts the list by Path, breaking ties by comparing Version fields.
144147
// The Version fields are interpreted as semantic versions (using semver.Compare)
145148
// optionally followed by a tie-breaking suffix introduced by a slash character,
146-
// like in "v0.0.1/go.mod".
149+
// like in "v0.0.1/module.cue".
147150
func Sort(list []Version) {
148151
sort.Slice(list, func(i, j int) bool {
149152
mi := list[i]
150153
mj := list[j]
151-
if mi.Path != mj.Path {
152-
return mi.Path < mj.Path
154+
if mi.path != mj.path {
155+
return mi.path < mj.path
153156
}
154157
// To help go.sum formatting, allow version/file.
155158
// Compare semver prefix by semver rules,
156159
// file by string order.
157-
vi := mi.Version
158-
vj := mj.Version
160+
vi := mi.version
161+
vj := mj.version
159162
var fi, fj string
160163
if k := strings.Index(vi, "/"); k >= 0 {
161164
vi, fi = vi[:k], vi[k:]

0 commit comments

Comments
 (0)