]> git.ipfire.org Git - thirdparty/gcc.git/blame - libgo/go/golang.org/x/mod/module/module.go
libgo: update to Go1.14beta1
[thirdparty/gcc.git] / libgo / go / golang.org / x / mod / module / module.go
CommitLineData
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
85package 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
98import (
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 111type 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).
128func (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.
136type 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 144func 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
156func (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
166func (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.
173type 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.
181func (e *InvalidVersionError) noun() string {
182 if e.Pseudo {
183 return "pseudo-version"
184 }
185 return "version"
186}
187
188func (e *InvalidVersionError) Error() string {
189 return fmt.Sprintf("%s %q invalid: %s", e.noun(), e.Version, e.Err)
190}
191
192func (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.".
200func 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.
220func 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
231func 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
246func 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
282func 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
325func 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).
338func 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).
371func 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
420func 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
429var 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
461func 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.
485func 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.
511func 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
517func 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.
553func 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".
572func 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
584func 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 612func 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 623func 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 633func 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.
663func 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
678func 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 689func 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}