]>
Commit | Line | Data |
---|---|---|
dd931d9b ILT |
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 | ||
5a8ea165 ILT |
5 | // Package module defines the module.Version type along with support code. |
6 | // | |
7 | // The module.Version type is a simple Path, Version pair: | |
8 | // | |
9 | // type Version struct { | |
10 | // Path string | |
11 | // Version string | |
12 | // } | |
13 | // | |
14 | // There are no restrictions imposed directly by use of this structure, | |
15 | // but additional checking functions, most notably Check, verify that | |
16 | // a particular path, version pair is valid. | |
17 | // | |
18 | // Escaped Paths | |
19 | // | |
20 | // Module paths appear as substrings of file system paths | |
21 | // (in the download cache) and of web server URLs in the proxy protocol. | |
22 | // In general we cannot rely on file systems to be case-sensitive, | |
23 | // nor can we rely on web servers, since they read from file systems. | |
24 | // That is, we cannot rely on the file system to keep rsc.io/QUOTE | |
25 | // and rsc.io/quote separate. Windows and macOS don't. | |
26 | // Instead, we must never require two different casings of a file path. | |
27 | // Because we want the download cache to match the proxy protocol, | |
28 | // and because we want the proxy protocol to be possible to serve | |
29 | // from a tree of static files (which might be stored on a case-insensitive | |
30 | // file system), the proxy protocol must never require two different casings | |
31 | // of a URL path either. | |
32 | // | |
33 | // One possibility would be to make the escaped form be the lowercase | |
34 | // hexadecimal encoding of the actual path bytes. This would avoid ever | |
35 | // needing different casings of a file path, but it would be fairly illegible | |
36 | // to most programmers when those paths appeared in the file system | |
37 | // (including in file paths in compiler errors and stack traces) | |
38 | // in web server logs, and so on. Instead, we want a safe escaped form that | |
39 | // leaves most paths unaltered. | |
40 | // | |
41 | // The safe escaped form is to replace every uppercase letter | |
42 | // with an exclamation mark followed by the letter's lowercase equivalent. | |
43 | // | |
44 | // For example, | |
45 | // | |
46 | // github.com/Azure/azure-sdk-for-go -> github.com/!azure/azure-sdk-for-go. | |
47 | // github.com/GoogleCloudPlatform/cloudsql-proxy -> github.com/!google!cloud!platform/cloudsql-proxy | |
48 | // github.com/Sirupsen/logrus -> github.com/!sirupsen/logrus. | |
49 | // | |
50 | // Import paths that avoid upper-case letters are left unchanged. | |
51 | // Note that because import paths are ASCII-only and avoid various | |
52 | // problematic punctuation (like : < and >), the escaped form is also ASCII-only | |
53 | // and avoids the same problematic punctuation. | |
54 | // | |
55 | // Import paths have never allowed exclamation marks, so there is no | |
56 | // need to define how to escape a literal !. | |
57 | // | |
58 | // Unicode Restrictions | |
59 | // | |
60 | // Today, paths are disallowed from using Unicode. | |
61 | // | |
62 | // Although paths are currently disallowed from using Unicode, | |
63 | // we would like at some point to allow Unicode letters as well, to assume that | |
64 | // file systems and URLs are Unicode-safe (storing UTF-8), and apply | |
65 | // the !-for-uppercase convention for escaping them in the file system. | |
66 | // But there are at least two subtle considerations. | |
67 | // | |
68 | // First, note that not all case-fold equivalent distinct runes | |
69 | // form an upper/lower pair. | |
70 | // For example, U+004B ('K'), U+006B ('k'), and U+212A ('K' for Kelvin) | |
71 | // are three distinct runes that case-fold to each other. | |
72 | // When we do add Unicode letters, we must not assume that upper/lower | |
73 | // are the only case-equivalent pairs. | |
74 | // Perhaps the Kelvin symbol would be disallowed entirely, for example. | |
75 | // Or perhaps it would escape as "!!k", or perhaps as "(212A)". | |
76 | // | |
77 | // Second, it would be nice to allow Unicode marks as well as letters, | |
78 | // but marks include combining marks, and then we must deal not | |
79 | // only with case folding but also normalization: both U+00E9 ('é') | |
80 | // and U+0065 U+0301 ('e' followed by combining acute accent) | |
81 | // look the same on the page and are treated by some file systems | |
82 | // as the same path. If we do allow Unicode marks in paths, there | |
83 | // must be some kind of normalization to allow only one canonical | |
84 | // encoding of any character used in an import path. | |
dd931d9b ILT |
85 | package module |
86 | ||
87 | // IMPORTANT NOTE | |
88 | // | |
89 | // This file essentially defines the set of valid import paths for the go command. | |
90 | // There are many subtle considerations, including Unicode ambiguity, | |
91 | // security, network, and file system representations. | |
92 | // | |
93 | // This file also defines the set of valid module path and version combinations, | |
94 | // another topic with many subtle considerations. | |
95 | // | |
96 | // Changes to the semantics in this file require approval from rsc. | |
97 | ||
98 | import ( | |
99 | "fmt" | |
100 | "sort" | |
101 | "strings" | |
102 | "unicode" | |
103 | "unicode/utf8" | |
104 | ||
5a8ea165 ILT |
105 | "golang.org/x/mod/semver" |
106 | errors "golang.org/x/xerrors" | |
dd931d9b ILT |
107 | ) |
108 | ||
5a8ea165 ILT |
109 | // A Version (for clients, a module.Version) is defined by a module path and version pair. |
110 | // These are stored in their plain (unescaped) form. | |
dd931d9b | 111 | type Version struct { |
5a8ea165 | 112 | // Path is a module path, like "golang.org/x/text" or "rsc.io/quote/v2". |
dd931d9b ILT |
113 | Path string |
114 | ||
115 | // Version is usually a semantic version in canonical form. | |
5a8ea165 | 116 | // There are three exceptions to this general rule. |
dd931d9b ILT |
117 | // First, the top-level target of a build has no specific version |
118 | // and uses Version = "". | |
119 | // Second, during MVS calculations the version "none" is used | |
120 | // to represent the decision to take no version of a given module. | |
5a8ea165 ILT |
121 | // Third, filesystem paths found in "replace" directives are |
122 | // represented by a path with an empty version. | |
dd931d9b ILT |
123 | Version string `json:",omitempty"` |
124 | } | |
125 | ||
5a8ea165 ILT |
126 | // String returns a representation of the Version suitable for logging |
127 | // (Path@Version, or just Path if Version is empty). | |
128 | func (m Version) String() string { | |
129 | if m.Version == "" { | |
130 | return m.Path | |
131 | } | |
132 | return m.Path + "@" + m.Version | |
133 | } | |
134 | ||
aa8901e9 ILT |
135 | // A ModuleError indicates an error specific to a module. |
136 | type ModuleError struct { | |
137 | Path string | |
138 | Version string | |
139 | Err error | |
140 | } | |
141 | ||
5a8ea165 ILT |
142 | // VersionError returns a ModuleError derived from a Version and error, |
143 | // or err itself if it is already such an error. | |
aa8901e9 | 144 | func VersionError(v Version, err error) error { |
5a8ea165 ILT |
145 | var mErr *ModuleError |
146 | if errors.As(err, &mErr) && mErr.Path == v.Path && mErr.Version == v.Version { | |
147 | return err | |
148 | } | |
aa8901e9 ILT |
149 | return &ModuleError{ |
150 | Path: v.Path, | |
151 | Version: v.Version, | |
152 | Err: err, | |
153 | } | |
154 | } | |
155 | ||
156 | func (e *ModuleError) Error() string { | |
157 | if v, ok := e.Err.(*InvalidVersionError); ok { | |
158 | return fmt.Sprintf("%s@%s: invalid %s: %v", e.Path, v.Version, v.noun(), v.Err) | |
159 | } | |
160 | if e.Version != "" { | |
161 | return fmt.Sprintf("%s@%s: %v", e.Path, e.Version, e.Err) | |
162 | } | |
163 | return fmt.Sprintf("module %s: %v", e.Path, e.Err) | |
164 | } | |
165 | ||
166 | func (e *ModuleError) Unwrap() error { return e.Err } | |
167 | ||
168 | // An InvalidVersionError indicates an error specific to a version, with the | |
169 | // module path unknown or specified externally. | |
170 | // | |
171 | // A ModuleError may wrap an InvalidVersionError, but an InvalidVersionError | |
172 | // must not wrap a ModuleError. | |
173 | type InvalidVersionError struct { | |
174 | Version string | |
175 | Pseudo bool | |
176 | Err error | |
177 | } | |
178 | ||
179 | // noun returns either "version" or "pseudo-version", depending on whether | |
180 | // e.Version is a pseudo-version. | |
181 | func (e *InvalidVersionError) noun() string { | |
182 | if e.Pseudo { | |
183 | return "pseudo-version" | |
184 | } | |
185 | return "version" | |
186 | } | |
187 | ||
188 | func (e *InvalidVersionError) Error() string { | |
189 | return fmt.Sprintf("%s %q invalid: %s", e.noun(), e.Version, e.Err) | |
190 | } | |
191 | ||
192 | func (e *InvalidVersionError) Unwrap() error { return e.Err } | |
193 | ||
dd931d9b ILT |
194 | // Check checks that a given module path, version pair is valid. |
195 | // In addition to the path being a valid module path | |
196 | // and the version being a valid semantic version, | |
197 | // the two must correspond. | |
198 | // For example, the path "yaml/v2" only corresponds to | |
199 | // semantic versions beginning with "v2.". | |
200 | func Check(path, version string) error { | |
201 | if err := CheckPath(path); err != nil { | |
202 | return err | |
203 | } | |
204 | if !semver.IsValid(version) { | |
aa8901e9 ILT |
205 | return &ModuleError{ |
206 | Path: path, | |
207 | Err: &InvalidVersionError{Version: version, Err: errors.New("not a semantic version")}, | |
208 | } | |
dd931d9b ILT |
209 | } |
210 | _, pathMajor, _ := SplitPathVersion(path) | |
5a8ea165 | 211 | if err := CheckPathMajor(version, pathMajor); err != nil { |
aa8901e9 | 212 | return &ModuleError{Path: path, Err: err} |
dd931d9b ILT |
213 | } |
214 | return nil | |
215 | } | |
216 | ||
217 | // firstPathOK reports whether r can appear in the first element of a module path. | |
218 | // The first element of the path must be an LDH domain name, at least for now. | |
219 | // To avoid case ambiguity, the domain name must be entirely lower case. | |
220 | func firstPathOK(r rune) bool { | |
221 | return r == '-' || r == '.' || | |
222 | '0' <= r && r <= '9' || | |
223 | 'a' <= r && r <= 'z' | |
224 | } | |
225 | ||
226 | // pathOK reports whether r can appear in an import path element. | |
227 | // Paths can be ASCII letters, ASCII digits, and limited ASCII punctuation: + - . _ and ~. | |
228 | // This matches what "go get" has historically recognized in import paths. | |
229 | // TODO(rsc): We would like to allow Unicode letters, but that requires additional | |
5a8ea165 | 230 | // care in the safe encoding (see "escaped paths" above). |
dd931d9b ILT |
231 | func pathOK(r rune) bool { |
232 | if r < utf8.RuneSelf { | |
233 | return r == '+' || r == '-' || r == '.' || r == '_' || r == '~' || | |
234 | '0' <= r && r <= '9' || | |
235 | 'A' <= r && r <= 'Z' || | |
236 | 'a' <= r && r <= 'z' | |
237 | } | |
238 | return false | |
239 | } | |
240 | ||
241 | // fileNameOK reports whether r can appear in a file name. | |
242 | // For now we allow all Unicode letters but otherwise limit to pathOK plus a few more punctuation characters. | |
243 | // If we expand the set of allowed characters here, we have to | |
244 | // work harder at detecting potential case-folding and normalization collisions. | |
5a8ea165 | 245 | // See note about "escaped paths" above. |
dd931d9b ILT |
246 | func fileNameOK(r rune) bool { |
247 | if r < utf8.RuneSelf { | |
248 | // Entire set of ASCII punctuation, from which we remove characters: | |
249 | // ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~ | |
250 | // We disallow some shell special characters: " ' * < > ? ` | | |
251 | // (Note that some of those are disallowed by the Windows file system as well.) | |
252 | // We also disallow path separators / : and \ (fileNameOK is only called on path element characters). | |
253 | // We allow spaces (U+0020) in file names. | |
254 | const allowed = "!#$%&()+,-.=@[]^_{}~ " | |
255 | if '0' <= r && r <= '9' || 'A' <= r && r <= 'Z' || 'a' <= r && r <= 'z' { | |
256 | return true | |
257 | } | |
258 | for i := 0; i < len(allowed); i++ { | |
259 | if rune(allowed[i]) == r { | |
260 | return true | |
261 | } | |
262 | } | |
263 | return false | |
264 | } | |
265 | // It may be OK to add more ASCII punctuation here, but only carefully. | |
266 | // For example Windows disallows < > \, and macOS disallows :, so we must not allow those. | |
267 | return unicode.IsLetter(r) | |
268 | } | |
269 | ||
270 | // CheckPath checks that a module path is valid. | |
5a8ea165 ILT |
271 | // A valid module path is a valid import path, as checked by CheckImportPath, |
272 | // with two additional constraints. | |
273 | // First, the leading path element (up to the first slash, if any), | |
274 | // by convention a domain name, must contain only lower-case ASCII letters, | |
275 | // ASCII digits, dots (U+002E), and dashes (U+002D); | |
276 | // it must contain at least one dot and cannot start with a dash. | |
277 | // Second, for a final path element of the form /vN, where N looks numeric | |
278 | // (ASCII digits and dots) must not begin with a leading zero, must not be /v1, | |
279 | // and must not contain any dots. For paths beginning with "gopkg.in/", | |
280 | // this second requirement is replaced by a requirement that the path | |
281 | // follow the gopkg.in server's conventions. | |
dd931d9b ILT |
282 | func CheckPath(path string) error { |
283 | if err := checkPath(path, false); err != nil { | |
284 | return fmt.Errorf("malformed module path %q: %v", path, err) | |
285 | } | |
286 | i := strings.Index(path, "/") | |
287 | if i < 0 { | |
288 | i = len(path) | |
289 | } | |
290 | if i == 0 { | |
291 | return fmt.Errorf("malformed module path %q: leading slash", path) | |
292 | } | |
293 | if !strings.Contains(path[:i], ".") { | |
294 | return fmt.Errorf("malformed module path %q: missing dot in first path element", path) | |
295 | } | |
296 | if path[0] == '-' { | |
297 | return fmt.Errorf("malformed module path %q: leading dash in first path element", path) | |
298 | } | |
299 | for _, r := range path[:i] { | |
300 | if !firstPathOK(r) { | |
301 | return fmt.Errorf("malformed module path %q: invalid char %q in first path element", path, r) | |
302 | } | |
303 | } | |
304 | if _, _, ok := SplitPathVersion(path); !ok { | |
305 | return fmt.Errorf("malformed module path %q: invalid version", path) | |
306 | } | |
307 | return nil | |
308 | } | |
309 | ||
310 | // CheckImportPath checks that an import path is valid. | |
5a8ea165 ILT |
311 | // |
312 | // A valid import path consists of one or more valid path elements | |
313 | // separated by slashes (U+002F). (It must not begin with nor end in a slash.) | |
314 | // | |
315 | // A valid path element is a non-empty string made up of | |
316 | // ASCII letters, ASCII digits, and limited ASCII punctuation: + - . _ and ~. | |
317 | // It must not begin or end with a dot (U+002E), nor contain two dots in a row. | |
318 | // | |
319 | // The element prefix up to the first dot must not be a reserved file name | |
320 | // on Windows, regardless of case (CON, com1, NuL, and so on). | |
321 | // | |
322 | // CheckImportPath may be less restrictive in the future, but see the | |
323 | // top-level package documentation for additional information about | |
324 | // subtleties of Unicode. | |
dd931d9b ILT |
325 | func CheckImportPath(path string) error { |
326 | if err := checkPath(path, false); err != nil { | |
327 | return fmt.Errorf("malformed import path %q: %v", path, err) | |
328 | } | |
329 | return nil | |
330 | } | |
331 | ||
332 | // checkPath checks that a general path is valid. | |
333 | // It returns an error describing why but not mentioning path. | |
334 | // Because these checks apply to both module paths and import paths, | |
335 | // the caller is expected to add the "malformed ___ path %q: " prefix. | |
336 | // fileName indicates whether the final element of the path is a file name | |
337 | // (as opposed to a directory name). | |
338 | func checkPath(path string, fileName bool) error { | |
339 | if !utf8.ValidString(path) { | |
340 | return fmt.Errorf("invalid UTF-8") | |
341 | } | |
342 | if path == "" { | |
343 | return fmt.Errorf("empty string") | |
344 | } | |
aa8901e9 ILT |
345 | if path[0] == '-' { |
346 | return fmt.Errorf("leading dash") | |
347 | } | |
dd931d9b ILT |
348 | if strings.Contains(path, "//") { |
349 | return fmt.Errorf("double slash") | |
350 | } | |
351 | if path[len(path)-1] == '/' { | |
352 | return fmt.Errorf("trailing slash") | |
353 | } | |
354 | elemStart := 0 | |
355 | for i, r := range path { | |
356 | if r == '/' { | |
357 | if err := checkElem(path[elemStart:i], fileName); err != nil { | |
358 | return err | |
359 | } | |
360 | elemStart = i + 1 | |
361 | } | |
362 | } | |
363 | if err := checkElem(path[elemStart:], fileName); err != nil { | |
364 | return err | |
365 | } | |
366 | return nil | |
367 | } | |
368 | ||
369 | // checkElem checks whether an individual path element is valid. | |
370 | // fileName indicates whether the element is a file name (not a directory name). | |
371 | func checkElem(elem string, fileName bool) error { | |
372 | if elem == "" { | |
373 | return fmt.Errorf("empty path element") | |
374 | } | |
375 | if strings.Count(elem, ".") == len(elem) { | |
376 | return fmt.Errorf("invalid path element %q", elem) | |
377 | } | |
378 | if elem[0] == '.' && !fileName { | |
379 | return fmt.Errorf("leading dot in path element") | |
380 | } | |
381 | if elem[len(elem)-1] == '.' { | |
382 | return fmt.Errorf("trailing dot in path element") | |
383 | } | |
384 | charOK := pathOK | |
385 | if fileName { | |
386 | charOK = fileNameOK | |
387 | } | |
388 | for _, r := range elem { | |
389 | if !charOK(r) { | |
390 | return fmt.Errorf("invalid char %q", r) | |
391 | } | |
392 | } | |
393 | ||
394 | // Windows disallows a bunch of path elements, sadly. | |
395 | // See https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file | |
396 | short := elem | |
397 | if i := strings.Index(short, "."); i >= 0 { | |
398 | short = short[:i] | |
399 | } | |
400 | for _, bad := range badWindowsNames { | |
401 | if strings.EqualFold(bad, short) { | |
4f4a855d | 402 | return fmt.Errorf("%q disallowed as path element component on Windows", short) |
dd931d9b ILT |
403 | } |
404 | } | |
405 | return nil | |
406 | } | |
407 | ||
5a8ea165 ILT |
408 | // CheckFilePath checks that a slash-separated file path is valid. |
409 | // The definition of a valid file path is the same as the definition | |
410 | // of a valid import path except that the set of allowed characters is larger: | |
411 | // all Unicode letters, ASCII digits, the ASCII space character (U+0020), | |
412 | // and the ASCII punctuation characters | |
413 | // “!#$%&()+,-.=@[]^_{}~”. | |
414 | // (The excluded punctuation characters, " * < > ? ` ' | / \ and :, | |
415 | // have special meanings in certain shells or operating systems.) | |
416 | // | |
417 | // CheckFilePath may be less restrictive in the future, but see the | |
418 | // top-level package documentation for additional information about | |
419 | // subtleties of Unicode. | |
dd931d9b ILT |
420 | func CheckFilePath(path string) error { |
421 | if err := checkPath(path, true); err != nil { | |
422 | return fmt.Errorf("malformed file path %q: %v", path, err) | |
423 | } | |
424 | return nil | |
425 | } | |
426 | ||
427 | // badWindowsNames are the reserved file path elements on Windows. | |
428 | // See https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file | |
429 | var badWindowsNames = []string{ | |
430 | "CON", | |
431 | "PRN", | |
432 | "AUX", | |
433 | "NUL", | |
434 | "COM1", | |
435 | "COM2", | |
436 | "COM3", | |
437 | "COM4", | |
438 | "COM5", | |
439 | "COM6", | |
440 | "COM7", | |
441 | "COM8", | |
442 | "COM9", | |
443 | "LPT1", | |
444 | "LPT2", | |
445 | "LPT3", | |
446 | "LPT4", | |
447 | "LPT5", | |
448 | "LPT6", | |
449 | "LPT7", | |
450 | "LPT8", | |
451 | "LPT9", | |
452 | } | |
453 | ||
454 | // SplitPathVersion returns prefix and major version such that prefix+pathMajor == path | |
455 | // and version is either empty or "/vN" for N >= 2. | |
456 | // As a special case, gopkg.in paths are recognized directly; | |
457 | // they require ".vN" instead of "/vN", and for all N, not just N >= 2. | |
5a8ea165 ILT |
458 | // SplitPathVersion returns with ok = false when presented with |
459 | // a path whose last path element does not satisfy the constraints | |
460 | // applied by CheckPath, such as "example.com/pkg/v1" or "example.com/pkg/v1.2". | |
dd931d9b ILT |
461 | func SplitPathVersion(path string) (prefix, pathMajor string, ok bool) { |
462 | if strings.HasPrefix(path, "gopkg.in/") { | |
463 | return splitGopkgIn(path) | |
464 | } | |
465 | ||
466 | i := len(path) | |
467 | dot := false | |
468 | for i > 0 && ('0' <= path[i-1] && path[i-1] <= '9' || path[i-1] == '.') { | |
469 | if path[i-1] == '.' { | |
470 | dot = true | |
471 | } | |
472 | i-- | |
473 | } | |
4f4a855d | 474 | if i <= 1 || i == len(path) || path[i-1] != 'v' || path[i-2] != '/' { |
dd931d9b ILT |
475 | return path, "", true |
476 | } | |
477 | prefix, pathMajor = path[:i-2], path[i-2:] | |
478 | if dot || len(pathMajor) <= 2 || pathMajor[2] == '0' || pathMajor == "/v1" { | |
479 | return path, "", false | |
480 | } | |
481 | return prefix, pathMajor, true | |
482 | } | |
483 | ||
484 | // splitGopkgIn is like SplitPathVersion but only for gopkg.in paths. | |
485 | func splitGopkgIn(path string) (prefix, pathMajor string, ok bool) { | |
486 | if !strings.HasPrefix(path, "gopkg.in/") { | |
487 | return path, "", false | |
488 | } | |
489 | i := len(path) | |
490 | if strings.HasSuffix(path, "-unstable") { | |
491 | i -= len("-unstable") | |
492 | } | |
493 | for i > 0 && ('0' <= path[i-1] && path[i-1] <= '9') { | |
494 | i-- | |
495 | } | |
496 | if i <= 1 || path[i-1] != 'v' || path[i-2] != '.' { | |
497 | // All gopkg.in paths must end in vN for some N. | |
498 | return path, "", false | |
499 | } | |
500 | prefix, pathMajor = path[:i-2], path[i-2:] | |
501 | if len(pathMajor) <= 2 || pathMajor[2] == '0' && pathMajor != ".v0" { | |
502 | return path, "", false | |
503 | } | |
504 | return prefix, pathMajor, true | |
505 | } | |
506 | ||
5a8ea165 ILT |
507 | // MatchPathMajor reports whether the semantic version v |
508 | // matches the path major version pathMajor. | |
509 | // | |
510 | // MatchPathMajor returns true if and only if CheckPathMajor returns nil. | |
511 | func MatchPathMajor(v, pathMajor string) bool { | |
512 | return CheckPathMajor(v, pathMajor) == nil | |
513 | } | |
514 | ||
515 | // CheckPathMajor returns a non-nil error if the semantic version v | |
aa8901e9 | 516 | // does not match the path major version pathMajor. |
5a8ea165 ILT |
517 | func CheckPathMajor(v, pathMajor string) error { |
518 | // TODO(jayconrod): return errors or panic for invalid inputs. This function | |
519 | // (and others) was covered by integration tests for cmd/go, and surrounding | |
520 | // code protected against invalid inputs like non-canonical versions. | |
dd931d9b ILT |
521 | if strings.HasPrefix(pathMajor, ".v") && strings.HasSuffix(pathMajor, "-unstable") { |
522 | pathMajor = strings.TrimSuffix(pathMajor, "-unstable") | |
523 | } | |
524 | if strings.HasPrefix(v, "v0.0.0-") && pathMajor == ".v1" { | |
525 | // Allow old bug in pseudo-versions that generated v0.0.0- pseudoversion for gopkg .v1. | |
526 | // For example, gopkg.in/yaml.v2@v2.2.1's go.mod requires gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405. | |
aa8901e9 | 527 | return nil |
dd931d9b ILT |
528 | } |
529 | m := semver.Major(v) | |
530 | if pathMajor == "" { | |
aa8901e9 ILT |
531 | if m == "v0" || m == "v1" || semver.Build(v) == "+incompatible" { |
532 | return nil | |
533 | } | |
534 | pathMajor = "v0 or v1" | |
535 | } else if pathMajor[0] == '/' || pathMajor[0] == '.' { | |
536 | if m == pathMajor[1:] { | |
537 | return nil | |
538 | } | |
539 | pathMajor = pathMajor[1:] | |
540 | } | |
541 | return &InvalidVersionError{ | |
542 | Version: v, | |
543 | Err: fmt.Errorf("should be %s, not %s", pathMajor, semver.Major(v)), | |
dd931d9b | 544 | } |
aa8901e9 ILT |
545 | } |
546 | ||
547 | // PathMajorPrefix returns the major-version tag prefix implied by pathMajor. | |
548 | // An empty PathMajorPrefix allows either v0 or v1. | |
549 | // | |
550 | // Note that MatchPathMajor may accept some versions that do not actually begin | |
551 | // with this prefix: namely, it accepts a 'v0.0.0-' prefix for a '.v1' | |
552 | // pathMajor, even though that pathMajor implies 'v1' tagging. | |
553 | func PathMajorPrefix(pathMajor string) string { | |
554 | if pathMajor == "" { | |
555 | return "" | |
556 | } | |
557 | if pathMajor[0] != '/' && pathMajor[0] != '.' { | |
558 | panic("pathMajor suffix " + pathMajor + " passed to PathMajorPrefix lacks separator") | |
559 | } | |
560 | if strings.HasPrefix(pathMajor, ".v") && strings.HasSuffix(pathMajor, "-unstable") { | |
561 | pathMajor = strings.TrimSuffix(pathMajor, "-unstable") | |
562 | } | |
563 | m := pathMajor[1:] | |
564 | if m != semver.Major(m) { | |
565 | panic("pathMajor suffix " + pathMajor + "passed to PathMajorPrefix is not a valid major version") | |
566 | } | |
567 | return m | |
dd931d9b ILT |
568 | } |
569 | ||
570 | // CanonicalVersion returns the canonical form of the version string v. | |
571 | // It is the same as semver.Canonical(v) except that it preserves the special build suffix "+incompatible". | |
572 | func CanonicalVersion(v string) string { | |
573 | cv := semver.Canonical(v) | |
574 | if semver.Build(v) == "+incompatible" { | |
575 | cv += "+incompatible" | |
576 | } | |
577 | return cv | |
578 | } | |
579 | ||
5a8ea165 ILT |
580 | // Sort sorts the list by Path, breaking ties by comparing Version fields. |
581 | // The Version fields are interpreted as semantic versions (using semver.Compare) | |
582 | // optionally followed by a tie-breaking suffix introduced by a slash character, | |
583 | // like in "v0.0.1/go.mod". | |
dd931d9b ILT |
584 | func Sort(list []Version) { |
585 | sort.Slice(list, func(i, j int) bool { | |
586 | mi := list[i] | |
587 | mj := list[j] | |
588 | if mi.Path != mj.Path { | |
589 | return mi.Path < mj.Path | |
590 | } | |
591 | // To help go.sum formatting, allow version/file. | |
592 | // Compare semver prefix by semver rules, | |
593 | // file by string order. | |
594 | vi := mi.Version | |
595 | vj := mj.Version | |
596 | var fi, fj string | |
597 | if k := strings.Index(vi, "/"); k >= 0 { | |
598 | vi, fi = vi[:k], vi[k:] | |
599 | } | |
600 | if k := strings.Index(vj, "/"); k >= 0 { | |
601 | vj, fj = vj[:k], vj[k:] | |
602 | } | |
603 | if vi != vj { | |
604 | return semver.Compare(vi, vj) < 0 | |
605 | } | |
606 | return fi < fj | |
607 | }) | |
608 | } | |
609 | ||
5a8ea165 | 610 | // EscapePath returns the escaped form of the given module path. |
dd931d9b | 611 | // It fails if the module path is invalid. |
5a8ea165 | 612 | func EscapePath(path string) (escaped string, err error) { |
dd931d9b ILT |
613 | if err := CheckPath(path); err != nil { |
614 | return "", err | |
615 | } | |
616 | ||
5a8ea165 | 617 | return escapeString(path) |
dd931d9b ILT |
618 | } |
619 | ||
5a8ea165 | 620 | // EscapeVersion returns the escaped form of the given module version. |
dd931d9b ILT |
621 | // Versions are allowed to be in non-semver form but must be valid file names |
622 | // and not contain exclamation marks. | |
5a8ea165 | 623 | func EscapeVersion(v string) (escaped string, err error) { |
dd931d9b | 624 | if err := checkElem(v, true); err != nil || strings.Contains(v, "!") { |
aa8901e9 ILT |
625 | return "", &InvalidVersionError{ |
626 | Version: v, | |
627 | Err: fmt.Errorf("disallowed version string"), | |
628 | } | |
dd931d9b | 629 | } |
5a8ea165 | 630 | return escapeString(v) |
dd931d9b ILT |
631 | } |
632 | ||
5a8ea165 | 633 | func escapeString(s string) (escaped string, err error) { |
dd931d9b ILT |
634 | haveUpper := false |
635 | for _, r := range s { | |
636 | if r == '!' || r >= utf8.RuneSelf { | |
637 | // This should be disallowed by CheckPath, but diagnose anyway. | |
5a8ea165 ILT |
638 | // The correctness of the escaping loop below depends on it. |
639 | return "", fmt.Errorf("internal error: inconsistency in EscapePath") | |
dd931d9b ILT |
640 | } |
641 | if 'A' <= r && r <= 'Z' { | |
642 | haveUpper = true | |
643 | } | |
644 | } | |
645 | ||
646 | if !haveUpper { | |
647 | return s, nil | |
648 | } | |
649 | ||
650 | var buf []byte | |
651 | for _, r := range s { | |
652 | if 'A' <= r && r <= 'Z' { | |
653 | buf = append(buf, '!', byte(r+'a'-'A')) | |
654 | } else { | |
655 | buf = append(buf, byte(r)) | |
656 | } | |
657 | } | |
658 | return string(buf), nil | |
659 | } | |
660 | ||
5a8ea165 ILT |
661 | // UnescapePath returns the module path for the given escaped path. |
662 | // It fails if the escaped path is invalid or describes an invalid path. | |
663 | func UnescapePath(escaped string) (path string, err error) { | |
664 | path, ok := unescapeString(escaped) | |
dd931d9b | 665 | if !ok { |
5a8ea165 | 666 | return "", fmt.Errorf("invalid escaped module path %q", escaped) |
dd931d9b ILT |
667 | } |
668 | if err := CheckPath(path); err != nil { | |
5a8ea165 | 669 | return "", fmt.Errorf("invalid escaped module path %q: %v", escaped, err) |
dd931d9b ILT |
670 | } |
671 | return path, nil | |
672 | } | |
673 | ||
5a8ea165 ILT |
674 | // UnescapeVersion returns the version string for the given escaped version. |
675 | // It fails if the escaped form is invalid or describes an invalid version. | |
dd931d9b ILT |
676 | // Versions are allowed to be in non-semver form but must be valid file names |
677 | // and not contain exclamation marks. | |
5a8ea165 ILT |
678 | func UnescapeVersion(escaped string) (v string, err error) { |
679 | v, ok := unescapeString(escaped) | |
dd931d9b | 680 | if !ok { |
5a8ea165 | 681 | return "", fmt.Errorf("invalid escaped version %q", escaped) |
dd931d9b ILT |
682 | } |
683 | if err := checkElem(v, true); err != nil { | |
5a8ea165 | 684 | return "", fmt.Errorf("invalid escaped version %q: %v", v, err) |
dd931d9b ILT |
685 | } |
686 | return v, nil | |
687 | } | |
688 | ||
5a8ea165 | 689 | func unescapeString(escaped string) (string, bool) { |
dd931d9b ILT |
690 | var buf []byte |
691 | ||
692 | bang := false | |
5a8ea165 | 693 | for _, r := range escaped { |
dd931d9b ILT |
694 | if r >= utf8.RuneSelf { |
695 | return "", false | |
696 | } | |
697 | if bang { | |
698 | bang = false | |
699 | if r < 'a' || 'z' < r { | |
700 | return "", false | |
701 | } | |
702 | buf = append(buf, byte(r+'A'-'a')) | |
703 | continue | |
704 | } | |
705 | if r == '!' { | |
706 | bang = true | |
707 | continue | |
708 | } | |
709 | if 'A' <= r && r <= 'Z' { | |
710 | return "", false | |
711 | } | |
712 | buf = append(buf, byte(r)) | |
713 | } | |
714 | if bang { | |
715 | return "", false | |
716 | } | |
717 | return string(buf), true | |
718 | } |