1 package module
2
3 import (
4 "errors"
5 "fmt"
6 "regexp"
7 "strings"
8 "unicode"
9 "unicode/utf8"
10
11 "cuelang.org/go/internal/mod/semver"
12 )
13
14
15
16 var (
17 basePathPat = regexp.MustCompile(`^[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*$`)
18 tagPat = regexp.MustCompile(`^[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}$`)
19 )
20
21
22
23
24
25
26
27 func Check(path, version string) error {
28 if err := CheckPath(path); err != nil {
29 return err
30 }
31 if !semver.IsValid(version) {
32 return &ModuleError{
33 Path: path,
34 Err: &InvalidVersionError{Version: version, Err: errors.New("not a semantic version")},
35 }
36 }
37 _, pathMajor, _ := SplitPathVersion(path)
38 if err := CheckPathMajor(version, pathMajor); err != nil {
39 return &ModuleError{Path: path, Err: err}
40 }
41 return nil
42 }
43
44
45
46
47 func firstPathOK(r rune) bool {
48 return r == '-' || r == '.' ||
49 '0' <= r && r <= '9' ||
50 'a' <= r && r <= 'z'
51 }
52
53
54
55
56
57
58
59
60
61
62 func modPathOK(r rune) bool {
63 if r < utf8.RuneSelf {
64 return r == '-' || r == '.' || r == '_' || r == '~' ||
65 '0' <= r && r <= '9' ||
66 'A' <= r && r <= 'Z' ||
67 'a' <= r && r <= 'z'
68 }
69 return false
70 }
71
72
73
74
75
76
77
78
79 func importPathOK(r rune) bool {
80 return modPathOK(r) || r == '+'
81 }
82
83
84
85
86
87
88 func fileNameOK(r rune) bool {
89 if r < utf8.RuneSelf {
90
91
92
93
94
95
96 const allowed = "!#$%&()+,-.=@[]^_{}~ "
97 if '0' <= r && r <= '9' || 'A' <= r && r <= 'Z' || 'a' <= r && r <= 'z' {
98 return true
99 }
100 return strings.ContainsRune(allowed, r)
101 }
102
103
104 return unicode.IsLetter(r)
105 }
106
107
108
109 func CheckPathWithoutVersion(basePath string) (err error) {
110 if _, _, ok := SplitPathVersion(basePath); ok {
111 return fmt.Errorf("module path inappropriately contains major version")
112 }
113 if err := checkPath(basePath, modulePath); err != nil {
114 return err
115 }
116 i := strings.Index(basePath, "/")
117 if i < 0 {
118 i = len(basePath)
119 }
120 if i == 0 {
121 return fmt.Errorf("leading slash")
122 }
123 if !strings.Contains(basePath[:i], ".") {
124 return fmt.Errorf("missing dot in first path element")
125 }
126 if basePath[0] == '-' {
127 return fmt.Errorf("leading dash in first path element")
128 }
129 for _, r := range basePath[:i] {
130 if !firstPathOK(r) {
131 return fmt.Errorf("invalid char %q in first path element", r)
132 }
133 }
134
135 if !basePathPat.MatchString(basePath) {
136 return fmt.Errorf("non-conforming path %q", basePath)
137 }
138 return nil
139 }
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155 func CheckPath(mpath string) (err error) {
156 if mpath == "local" {
157 return nil
158 }
159 defer func() {
160 if err != nil {
161 err = &InvalidPathError{Kind: "module", Path: mpath, Err: err}
162 }
163 }()
164
165 basePath, vers, ok := SplitPathVersion(mpath)
166 if !ok {
167 return fmt.Errorf("no major version found in module path")
168 }
169 if semver.Major(vers) != vers {
170 return fmt.Errorf("path can contain major version only")
171 }
172 if err := CheckPathWithoutVersion(basePath); err != nil {
173 return err
174 }
175 if !tagPat.MatchString(vers) {
176 return fmt.Errorf("non-conforming version %q", vers)
177 }
178 return nil
179 }
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195 func CheckImportPath(path string) error {
196 parts := ParseImportPath(path)
197 if semver.Major(parts.Version) != parts.Version {
198 return &InvalidPathError{
199 Kind: "import",
200 Path: path,
201 Err: fmt.Errorf("import paths can only contain a major version specifier"),
202 }
203 }
204 if err := checkPath(parts.Path, importPath); err != nil {
205 return &InvalidPathError{Kind: "import", Path: path, Err: err}
206 }
207 return nil
208 }
209
210
211
212 type pathKind int
213
214 const (
215 modulePath pathKind = iota
216 importPath
217 filePath
218 )
219
220
221
222
223
224
225
226
227 func checkPath(path string, kind pathKind) error {
228 if !utf8.ValidString(path) {
229 return fmt.Errorf("invalid UTF-8")
230 }
231 if path == "" {
232 return fmt.Errorf("empty string")
233 }
234 if path[0] == '-' && kind != filePath {
235 return fmt.Errorf("leading dash")
236 }
237 if strings.Contains(path, "//") {
238 return fmt.Errorf("double slash")
239 }
240 if path[len(path)-1] == '/' {
241 return fmt.Errorf("trailing slash")
242 }
243 elemStart := 0
244 for i, r := range path {
245 if r == '/' {
246 if err := checkElem(path[elemStart:i], kind); err != nil {
247 return err
248 }
249 elemStart = i + 1
250 }
251 }
252 if err := checkElem(path[elemStart:], kind); err != nil {
253 return err
254 }
255 return nil
256 }
257
258
259 func checkElem(elem string, kind pathKind) error {
260 if elem == "" {
261 return fmt.Errorf("empty path element")
262 }
263 if strings.Count(elem, ".") == len(elem) {
264 return fmt.Errorf("invalid path element %q", elem)
265 }
266 if elem[0] == '.' && kind == modulePath {
267 return fmt.Errorf("leading dot in path element")
268 }
269 if elem[len(elem)-1] == '.' {
270 return fmt.Errorf("trailing dot in path element")
271 }
272 for _, r := range elem {
273 ok := false
274 switch kind {
275 case modulePath:
276 ok = modPathOK(r)
277 case importPath:
278 ok = importPathOK(r)
279 case filePath:
280 ok = fileNameOK(r)
281 default:
282 panic(fmt.Sprintf("internal error: invalid kind %v", kind))
283 }
284 if !ok {
285 return fmt.Errorf("invalid char %q", r)
286 }
287 }
288
289
290 short := elem
291 if i := strings.Index(short, "."); i >= 0 {
292 short = short[:i]
293 }
294 for _, bad := range badWindowsNames {
295 if strings.EqualFold(bad, short) {
296 return fmt.Errorf("%q disallowed as path element component on Windows", short)
297 }
298 }
299
300 if kind == filePath {
301
302
303 return nil
304 }
305
306
307
308 if tilde := strings.LastIndexByte(short, '~'); tilde >= 0 && tilde < len(short)-1 {
309 suffix := short[tilde+1:]
310 suffixIsDigits := true
311 for _, r := range suffix {
312 if r < '0' || r > '9' {
313 suffixIsDigits = false
314 break
315 }
316 }
317 if suffixIsDigits {
318 return fmt.Errorf("trailing tilde and digits in path element")
319 }
320 }
321
322 return nil
323 }
324
325
326
327
328
329
330
331
332
333
334
335
336
337 func CheckFilePath(path string) error {
338 if err := checkPath(path, filePath); err != nil {
339 return &InvalidPathError{Kind: "file", Path: path, Err: err}
340 }
341 return nil
342 }
343
344
345
346 var badWindowsNames = []string{
347 "CON",
348 "PRN",
349 "AUX",
350 "NUL",
351 "COM1",
352 "COM2",
353 "COM3",
354 "COM4",
355 "COM5",
356 "COM6",
357 "COM7",
358 "COM8",
359 "COM9",
360 "LPT1",
361 "LPT2",
362 "LPT3",
363 "LPT4",
364 "LPT5",
365 "LPT6",
366 "LPT7",
367 "LPT8",
368 "LPT9",
369 }
370
371
372
373
374
375
376
377
378 func SplitPathVersion(path string) (prefix, version string, ok bool) {
379 i := strings.LastIndex(path, "@")
380 split := i
381 if i <= 0 || i+2 >= len(path) {
382 return "", "", false
383 }
384 if strings.Contains(path[:i], "@") {
385 return "", "", false
386 }
387 if path[i+1] != 'v' {
388 return "", "", false
389 }
390 if !semver.IsValid(path[i+1:]) {
391 return "", "", false
392 }
393 return path[:split], path[split+1:], true
394 }
395
396
397 type ImportPath struct {
398
399
400 Path string
401
402
403
404
405
406 Version string
407
408
409
410
411
412 Qualifier string
413
414
415
416 ExplicitQualifier bool
417 }
418
419
420
421
422 func (parts ImportPath) Canonical() ImportPath {
423 if i := strings.LastIndex(parts.Path, "/"); i >= 0 && parts.Path[i+1:] == parts.Qualifier {
424 parts.Qualifier = ""
425 parts.ExplicitQualifier = false
426 }
427 return parts
428 }
429
430
431 func (parts ImportPath) Unqualified() ImportPath {
432 parts.Qualifier = ""
433 parts.ExplicitQualifier = false
434 return parts
435 }
436
437 func (parts ImportPath) String() string {
438 if parts.Version == "" && !parts.ExplicitQualifier {
439
440 return parts.Path
441 }
442 var buf strings.Builder
443 buf.WriteString(parts.Path)
444 if parts.Version != "" {
445 buf.WriteByte('@')
446 buf.WriteString(parts.Version)
447 }
448 if parts.ExplicitQualifier {
449 buf.WriteByte(':')
450 buf.WriteString(parts.Qualifier)
451 }
452 return buf.String()
453 }
454
455
456 func ParseImportPath(p string) ImportPath {
457 var parts ImportPath
458 pathWithoutQualifier := p
459 if i := strings.LastIndexAny(p, "/:"); i >= 0 && p[i] == ':' {
460 pathWithoutQualifier = p[:i]
461 parts.Qualifier = p[i+1:]
462 parts.ExplicitQualifier = true
463 }
464 parts.Path = pathWithoutQualifier
465 if path, version, ok := SplitPathVersion(pathWithoutQualifier); ok {
466 parts.Version = version
467 parts.Path = path
468 }
469 if !parts.ExplicitQualifier {
470 if i := strings.LastIndex(parts.Path, "/"); i >= 0 {
471 parts.Qualifier = parts.Path[i+1:]
472 } else {
473 parts.Qualifier = parts.Path
474 }
475 }
476 return parts
477 }
478
479
480
481 func CheckPathMajor(v, pathMajor string) error {
482 if m := semver.Major(v); m != pathMajor {
483 return &InvalidVersionError{
484 Version: v,
485 Err: fmt.Errorf("should be %s, not %s", pathMajor, m),
486 }
487 }
488 return nil
489 }
490
View as plain text