Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 11, 06:00 UTC
cmd: make splitModule strictly conform to Go module spec (#7974)
Per https://go.dev/ref/mod#go-mod-file-ident, '@' is not a valid module path character. The old implementation used strings.LastIndex to allow inputs like github.com/@user/module@v1.0.0 to parse, but such inputs are not valid module paths per the spec. Reject them explicitly. Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
Mohammed Al Sahaf committed
Sep 3, 2026 at 00:43 UTC
7cc98d4d3f9842410cc6a613ac46c590a4f4bb20
2 files changed
+105
-8
cmd/packagesfuncs.go
+16
-8
@@ -49,20 +49,28 @@ func cmdUpgrade(fl Flags) (int, error) {
49
func splitModule(arg string) (module, version string, err error) {
50
const versionSplit = "@"
51
52
- // accommodate module paths that have @ in them, but we can only tolerate that if there's also
53
- // a version, otherwise we don't know if it's a version separator or part of the file path
54
- lastVersionSplit := strings.LastIndex(arg, versionSplit)
55
- if lastVersionSplit < 0 {
52
+ // Per https://go.dev/ref/mod#go-mod-file-ident, module paths consist of
53
+ // ASCII letters, digits, and `-`, `.`, `_`, `~` only, so `@` is never a
54
+ // valid path character. That makes the split unambiguous: at most one
55
+ // `@` may appear, and it separates the module path from the version.
56
+ switch strings.Count(arg, versionSplit) {
57
+ case 0:
58
module = arg
57
- } else {
58
- module, version = arg[:lastVersionSplit], arg[lastVersionSplit+1:]
59
+ case 1:
60
+ idx := strings.Index(arg, versionSplit)
61
+ module, version = arg[:idx], arg[idx+1:]
62
+ if version == "" {
63
+ return "", "", fmt.Errorf("version is required after '@'")
64
+ }
65
+ default:
66
+ return "", "", fmt.Errorf("module path must not contain '@'")
67
}
68
69
if module == "" {
62
- err = fmt.Errorf("module name is required")
70
+ return "", "", fmt.Errorf("module name is required")
71
}
72
65
- return module, version, err
73
+ return module, version, nil
74
}
75
76
func cmdAddPackage(fl Flags) (int, error) {
cmd/packagesfuncs_split_test.go
new
+89
@@ -0,0 +1,89 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package caddycmd
16
+
17
+import (
18
+ "strings"
19
+ "testing"
20
+)
21
+
22
+// TestSplitModule_StrictGoSpec is the regression test for splitModule
23
+// silently accepting inputs that violate go.dev/ref/mod#go-mod-file-ident
24
+// (module paths cannot contain '@'). It also checks the happy paths and
25
+// the empty-version edge case.
26
+func TestSplitModule_StrictGoSpec(t *testing.T) {
27
+ for _, tt := range []struct {
28
+ name string
29
+ input string
30
+ module string
31
+ version string
32
+ wantErr bool
33
+ wantErrSub string
34
+ }{
35
+ {
36
+ name: "no version",
37
+ input: "github.com/caddyserver/caddy",
38
+ module: "github.com/caddyserver/caddy",
39
+ version: "",
40
+ },
41
+ {
42
+ name: "with version",
43
+ input: "github.com/caddyserver/caddy@v2.0.0",
44
+ module: "github.com/caddyserver/caddy",
45
+ version: "v2.0.0",
46
+ },
47
+ {
48
+ name: "multiple '@' rejected",
49
+ input: "github.com/@user/module@v1.0.0",
50
+ wantErr: true,
51
+ wantErrSub: "module path must not contain '@'",
52
+ },
53
+ {
54
+ name: "trailing '@' with empty version rejected",
55
+ input: "github.com/user/module@",
56
+ wantErr: true,
57
+ wantErrSub: "version is required after '@'",
58
+ },
59
+ {
60
+ name: "empty input rejected",
61
+ input: "",
62
+ wantErr: true,
63
+ wantErrSub: "module name is required",
64
+ },
65
+ } {
66
+ t.Run(tt.name, func(t *testing.T) {
67
+ module, version, err := splitModule(tt.input)
68
+
69
+ if tt.wantErr {
70
+ if err == nil {
71
+ t.Fatalf("expected error containing %q, got nil", tt.wantErrSub)
72
+ }
73
+ if !strings.Contains(err.Error(), tt.wantErrSub) {
74
+ t.Errorf("error %q does not contain %q", err.Error(), tt.wantErrSub)
75
+ }
76
+ return
77
+ }
78
+ if err != nil {
79
+ t.Fatalf("unexpected error: %v", err)
80
+ }
81
+ if module != tt.module {
82
+ t.Errorf("module: got %q, want %q", module, tt.module)
83
+ }
84
+ if version != tt.version {
85
+ t.Errorf("version: got %q, want %q", version, tt.version)
86
+ }
87
+ })
88
+ }
89
+}