diff --git a/CHANGELOG.md b/CHANGELOG.md index a36f5a3..7922247 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,5 @@ # 更新日志 -3.5.1 - 2025-06-09 ---- -- CHANGE: 大幅优化`Matcher`的性能, 实现零分配, 大幅提升性能; 单次操作时间: `254.3 ns/op` => `29.59 ns/op` - -25w45a - 2025-06-09 ---- -- PRE-RELEASE: 此版本是v3.5.1预发布版本,请勿在生产环境中使用; -- CHANGE: 大幅优化`Matcher`的性能, 实现零分配, 大幅提升性能; 单次操作时间: `254.3 ns/op` => `29.59 ns/op` - 3.5.0 - 2025-06-05 --- - CHANGE: 更新许可证 v2.0 => v2.1 diff --git a/DEV-VERSION b/DEV-VERSION index ebe0357..c4bec82 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w45a \ No newline at end of file +25w44a \ No newline at end of file diff --git a/VERSION b/VERSION index 3c8ff8c..e5b8203 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.5.1 \ No newline at end of file +3.5.0 \ No newline at end of file diff --git a/go.mod b/go.mod index 1ccfe73..90b925e 100644 --- a/go.mod +++ b/go.mod @@ -8,8 +8,8 @@ require ( github.com/WJQSERVER-STUDIO/logger v1.7.3 github.com/cloudwego/hertz v0.10.0 github.com/hertz-contrib/http2 v0.1.8 - golang.org/x/net v0.41.0 - golang.org/x/time v0.12.0 + golang.org/x/net v0.40.0 + golang.org/x/time v0.11.0 ) require ( @@ -36,10 +36,10 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - golang.org/x/arch v0.18.0 // indirect - golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect + golang.org/x/arch v0.17.0 // indirect + golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b // indirect golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.26.0 // indirect + golang.org/x/text v0.25.0 // indirect google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/go.sum b/go.sum index 4881472..834e6c7 100644 --- a/go.sum +++ b/go.sum @@ -89,16 +89,12 @@ github.com/wjqserver/modembed v0.0.1/go.mod h1:sYbQJMAjSBsdYQrUsuHY380XXE1CuRh8g github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= -golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= -golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b h1:QoALfVG9rhQ/M7vYDScfPdWjGL9dlsVVM5VGh7aKoAA= golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= -golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4= -golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -110,8 +106,6 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -141,12 +135,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index c50b7fb..c760b9a 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -110,7 +110,7 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx) } - if MatcherShell(u) && matchString(matcher) && cfg.Shell.Editor { + if MatcherShell(u) && matchString(matcher, matchedMatchers) && cfg.Shell.Editor { // 判断body是不是gzip var compress string if resp.Header.Get("Content-Encoding") == "gzip" { diff --git a/proxy/match.go b/proxy/match.go index 896a99b..cee4eaa 100644 --- a/proxy/match.go +++ b/proxy/match.go @@ -6,141 +6,8 @@ import ( "net/url" "regexp" "strings" - "sync" ) -var ( - githubPrefix = "https://github.com/" - rawPrefix = "https://raw.githubusercontent.com/" - gistPrefix = "https://gist.github.com/" - apiPrefix = "https://api.github.com/" - githubPrefixLen int - rawPrefixLen int - gistPrefixLen int - apiPrefixLen int -) - -func init() { - githubPrefixLen = len(githubPrefix) - rawPrefixLen = len(rawPrefix) - gistPrefixLen = len(gistPrefix) - apiPrefixLen = len(apiPrefix) - //log.Printf("githubPrefixLen: %d, rawPrefixLen: %d, gistPrefixLen: %d, apiPrefixLen: %d", githubPrefixLen, rawPrefixLen, gistPrefixLen, apiPrefixLen) -} - -// Matcher 从原始URL路径中高效地解析并匹配代理规则. -func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHProxyErrors) { - if len(rawPath) < 18 { - return "", "", "", NewErrorWithStatusLookup(404, "path too short") - } - - // 匹配 "https://github.com/" - if strings.HasPrefix(rawPath, githubPrefix) { - remaining := rawPath[githubPrefixLen:] - i := strings.IndexByte(remaining, '/') - if i <= 0 { - return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing user") - } - user := remaining[:i] - remaining = remaining[i+1:] - i = strings.IndexByte(remaining, '/') - if i <= 0 { - return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing repo") - } - repo := remaining[:i] - remaining = remaining[i+1:] - if len(remaining) == 0 { - return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing action") - } - i = strings.IndexByte(remaining, '/') - action := remaining - if i != -1 { - action = remaining[:i] - } - var matcher string - switch action { - case "releases", "archive": - matcher = "releases" - case "blob": - matcher = "blob" - case "raw": - matcher = "raw" - case "info", "git-upload-pack": - matcher = "clone" - default: - return "", "", "", NewErrorWithStatusLookup(400, fmt.Sprintf("unsupported github action: %s", action)) - } - return user, repo, matcher, nil - } - - // 匹配 "https://raw.githubusercontent.com/" - if strings.HasPrefix(rawPath, rawPrefix) { - remaining := rawPath[rawPrefixLen:] - // 这里的逻辑与 github.com 的类似, 需要提取 user, repo, branch, file... - // 我们只需要 user 和 repo - i := strings.IndexByte(remaining, '/') - if i <= 0 { - return "", "", "", NewErrorWithStatusLookup(400, "malformed raw url: missing user") - } - user := remaining[:i] - remaining = remaining[i+1:] - i = strings.IndexByte(remaining, '/') - if i <= 0 { - return "", "", "", NewErrorWithStatusLookup(400, "malformed raw url: missing repo") - } - repo := remaining[:i] - // raw 链接至少需要 user/repo/branch 三部分 - remaining = remaining[i+1:] - if len(remaining) == 0 { - return "", "", "", NewErrorWithStatusLookup(400, "malformed raw url: missing branch/commit") - } - return user, repo, "raw", nil - } - - // 匹配 "https://gist.github.com/" - if strings.HasPrefix(rawPath, gistPrefix) { - remaining := rawPath[gistPrefixLen:] - i := strings.IndexByte(remaining, '/') - if i <= 0 { - // case: https://gist.github.com/user - // 这种情况下, gist_id 缺失, 但我们仍然可以认为 user 是有效的 - if len(remaining) > 0 { - return remaining, "", "gist", nil - } - return "", "", "", NewErrorWithStatusLookup(400, "malformed gist url: missing user") - } - // case: https://gist.github.com/user/gist_id... - user := remaining[:i] - return user, "", "gist", nil - } - - // 匹配 "https://api.github.com/" - if strings.HasPrefix(rawPath, apiPrefix) { - if !cfg.Auth.ForceAllowApi && (cfg.Auth.Method != "header" || !cfg.Auth.Enabled) { - return "", "", "", NewErrorWithStatusLookup(403, "API proxy requires header authentication") - } - remaining := rawPath[apiPrefixLen:] - var user, repo string - if strings.HasPrefix(remaining, "repos/") { - parts := strings.SplitN(remaining[6:], "/", 3) - if len(parts) >= 2 { - user = parts[0] - repo = parts[1] - } - } else if strings.HasPrefix(remaining, "users/") { - parts := strings.SplitN(remaining[6:], "/", 2) - if len(parts) >= 1 { - user = parts[0] - } - } - return user, repo, "api", nil - } - - return "", "", "", NewErrorWithStatusLookup(404, "no matcher found for the given path") -} - -// 原实现 -/* func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHProxyErrors) { var ( user string @@ -150,11 +17,11 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro // 匹配 "https://github.com"开头的链接 if strings.HasPrefix(rawPath, "https://github.com") { remainingPath := strings.TrimPrefix(rawPath, "https://github.com") - - //if strings.HasPrefix(remainingPath, "/") { - // remainingPath = strings.TrimPrefix(remainingPath, "/") - //} - + /* + if strings.HasPrefix(remainingPath, "/") { + remainingPath = strings.TrimPrefix(remainingPath, "/") + } + */ remainingPath = strings.TrimPrefix(remainingPath, "/") // 预期格式/user/repo/more... // 取出user和repo和最后部分 @@ -236,56 +103,55 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro errMsg := "Didn't match any matcher" return "", "", "", NewErrorWithStatusLookup(404, errMsg) } -*/ var ( - proxyableMatchersMap map[string]struct{} - initMatchersOnce sync.Once + matchedMatchers = []string{ + "blob", + "raw", + "gist", + } ) -func initMatchers() { - initMatchersOnce.Do(func() { - matchers := []string{"blob", "raw", "gist"} - proxyableMatchersMap = make(map[string]struct{}, len(matchers)) - for _, m := range matchers { - proxyableMatchersMap[m] = struct{}{} - } - }) -} - -// matchString 与原始版本签名兼容 -func matchString(target string) bool { - initMatchers() - _, exists := proxyableMatchersMap[target] +// matchString 检查目标字符串是否在给定的字符串集合中 +func matchString(target string, stringsToMatch []string) bool { + matchMap := make(map[string]struct{}, len(stringsToMatch)) + for _, str := range stringsToMatch { + matchMap[str] = struct{}{} + } + _, exists := matchMap[target] return exists } -// extractParts 与原始版本签名兼容 +// extractParts 从给定的 URL 中提取所需的部分 func extractParts(rawURL string) (string, string, string, url.Values, error) { + // 解析 URL parsedURL, err := url.Parse(rawURL) if err != nil { return "", "", "", nil, err } - path := parsedURL.Path - if len(path) > 0 && path[0] == '/' { - path = path[1:] - } + // 获取路径部分并分割 + pathParts := strings.Split(parsedURL.Path, "/") - parts := strings.SplitN(path, "/", 3) - - if len(parts) < 2 { + // 提取所需的部分 + if len(pathParts) < 3 { return "", "", "", nil, fmt.Errorf("URL path is too short") } - repoOwner := "/" + parts[0] - repoName := "/" + parts[1] - var remainingPath string - if len(parts) > 2 { - remainingPath = "/" + parts[2] + // 提取 /WJQSERVER-STUDIO 和 /go-utils.git + repoOwner := "/" + pathParts[1] + repoName := "/" + pathParts[2] + + // 剩余部分 + remainingPath := strings.Join(pathParts[3:], "/") + if remainingPath != "" { + remainingPath = "/" + remainingPath } - return repoOwner, repoName, remainingPath, parsedURL.Query(), nil + // 查询参数 + queryParams := parsedURL.Query() + + return repoOwner, repoName, remainingPath, queryParams, nil } var urlPattern = regexp.MustCompile(`https?://[^\s'"]+`) diff --git a/proxy/matcher_test.go b/proxy/matcher_test.go deleted file mode 100644 index 3293817..0000000 --- a/proxy/matcher_test.go +++ /dev/null @@ -1,309 +0,0 @@ -package proxy - -import ( - "ghproxy/config" - "net/url" - "reflect" - "testing" -) - -func TestMatcher_Compatibility(t *testing.T) { - // --- 准备各种配置用于测试 --- - cfgWithAuth := &config.Config{ - Auth: config.AuthConfig{Enabled: true, Method: "header", ForceAllowApi: false}, - } - cfgNoAuth := &config.Config{ - Auth: config.AuthConfig{Enabled: false}, - } - cfgApiForceAllowed := &config.Config{ - Auth: config.AuthConfig{ForceAllowApi: true}, - } - cfgWrongAuthMethod := &config.Config{ - Auth: config.AuthConfig{Enabled: true, Method: "none"}, - } - - testCases := []struct { - name string - rawPath string - config *config.Config - expectedUser string - expectedRepo string - expectedMatcher string - expectError bool - expectedErrCode int - }{ - { - name: "GH Releases Path", - rawPath: "https://github.com/owner/repo/releases/download/v1.0/asset.zip", - config: cfgWithAuth, - expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "releases", - }, - { - name: "GH Archive Path", - rawPath: "https://github.com/owner/repo.git/archive/main.zip", - config: cfgWithAuth, - expectedUser: "owner", expectedRepo: "repo.git", expectedMatcher: "releases", - }, - { - name: "GH Blob Path", - rawPath: "https://github.com/owner/repo/blob/main/path/to/file.go", - config: cfgWithAuth, - expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "blob", - }, - { - name: "GH Raw Path", - rawPath: "https://github.com/owner/repo/raw/main/image.png", - config: cfgWithAuth, - expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "raw", - }, - { - name: "GH Clone Info Refs", - rawPath: "https://github.com/owner/repo.git/info/refs?service=git-upload-pack", - config: cfgWithAuth, - expectedUser: "owner", expectedRepo: "repo.git", expectedMatcher: "clone", - }, - { - name: "GH Clone Git Upload Pack", - rawPath: "https://github.com/owner/repo/git-upload-pack", - config: cfgWithAuth, - expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "clone", - }, - { - name: "Girhub Broken Path", - rawPath: "https://github.com/owner", - config: cfgWithAuth, - expectError: true, expectedErrCode: 400, - }, - - { - name: "RawGHUserContent Path", - rawPath: "https://raw.githubusercontent.com/owner/repo/branch/file.sh", - config: cfgWithAuth, - expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "raw", - }, - { - name: "Gist Path", - rawPath: "https://gist.github.com/user/abcdef1234567890", - config: cfgWithAuth, - expectedUser: "user", expectedRepo: "", expectedMatcher: "gist", - }, - { - name: "API Repos Path (with Auth)", - rawPath: "https://api.github.com/repos/owner/repo/pulls", - config: cfgWithAuth, - expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "api", - }, - { - name: "API Users Path (with Auth)", - rawPath: "https://api.github.com/users/someuser/repos", - config: cfgWithAuth, - expectedUser: "someuser", expectedRepo: "", expectedMatcher: "api", - }, - { - name: "API Other Path (with Auth)", - rawPath: "https://api.github.com/octocat", - config: cfgWithAuth, - expectedUser: "", expectedRepo: "", expectedMatcher: "api", - }, - { - name: "API Path (Force Allowed)", - rawPath: "https://api.github.com/repos/owner/repo", - config: cfgApiForceAllowed, // Auth disabled, but force allowed - expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "api", - }, - { - name: "Malformed GH Path (no repo)", - rawPath: "https://github.com/owner/", - config: cfgWithAuth, - expectError: true, expectedErrCode: 400, - }, - { - name: "Malformed GH Path (no action)", - rawPath: "https://github.com/owner/repo", - config: cfgWithAuth, - expectError: true, expectedErrCode: 400, - }, - { - name: "Malformed GH Path (empty user)", - rawPath: "https://github.com//repo/blob/main/file.go", - config: cfgWithAuth, - expectError: true, expectedErrCode: 400, - }, - { - name: "Malformed Raw Path (no repo)", - rawPath: "https://raw.githubusercontent.com/owner/", - config: cfgWithAuth, - expectError: true, expectedErrCode: 400, - }, - { - name: "Malformed Gist Path (no user)", - rawPath: "https://gist.github.com/", - config: cfgWithAuth, - expectError: true, expectedErrCode: 400, - }, - { - name: "Unsupported GH Action", - rawPath: "https://github.com/owner/repo/issues/123", - config: cfgWithAuth, - expectError: true, expectedErrCode: 400, - }, - { - name: "API Path (No Auth)", - rawPath: "https://api.github.com/user", - config: cfgNoAuth, - expectError: true, expectedErrCode: 403, - }, - { - name: "API Path (Wrong Auth Method)", - rawPath: "https://api.github.com/user", - config: cfgWrongAuthMethod, - expectError: true, expectedErrCode: 403, - }, - { - name: "No Matcher Found (other domain)", - rawPath: "https://bitbucket.org/owner/repo", - config: cfgWithAuth, - expectError: true, expectedErrCode: 404, - }, - { - name: "No Matcher Found (path too short)", - rawPath: "https://a.co", - config: cfgWithAuth, - expectError: true, expectedErrCode: 404, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - user, repo, matcher, ghErr := Matcher(tc.rawPath, tc.config) - - if tc.expectError { - if ghErr == nil { - t.Fatalf("Expected a GHProxyErrors error, but got nil") - } - if ghErr.StatusCode != tc.expectedErrCode { - t.Errorf("Expected error code %d, but got %d (msg: %s)", - tc.expectedErrCode, ghErr.StatusCode, ghErr.ErrorMessage) - } - } else { - if ghErr != nil { - t.Fatalf("Expected no error, but got: %s", ghErr.ErrorMessage) - } - if user != tc.expectedUser { - t.Errorf("user: got %q, want %q", user, tc.expectedUser) - } - if repo != tc.expectedRepo { - t.Errorf("repo: got %q, want %q", repo, tc.expectedRepo) - } - if matcher != tc.expectedMatcher { - t.Errorf("matcher: got %q, want %q", matcher, tc.expectedMatcher) - } - } - }) - } -} - -func TestExtractParts_Compatibility(t *testing.T) { - testCases := []struct { - name string - rawURL string - expectedOwner string - expectedRepo string - expectedRem string - expectedQuery url.Values - expectError bool - }{ - { - name: "Standard git clone URL", - rawURL: "https://github.com/WJQSERVER-STUDIO/go-utils.git/info/refs?service=git-upload-pack", - expectedOwner: "/WJQSERVER-STUDIO", - expectedRepo: "/go-utils.git", - expectedRem: "/info/refs", - expectedQuery: url.Values{"service": []string{"git-upload-pack"}}, - }, - { - name: "No remaining path", - rawURL: "https://example.com/owner/repo", - expectedOwner: "/owner", - expectedRepo: "/repo", - expectedRem: "", - expectedQuery: url.Values{}, - }, - { - name: "Root path only", - rawURL: "https://example.com/", - expectError: true, // Path is too short - }, - { - name: "One level path", - rawURL: "https://example.com/owner", - expectError: true, // Path is too short - }, - { - name: "Empty path segments", - rawURL: "https://example.com//repo/a", // Will be treated as /repo/a - expectedOwner: "", // First part is empty - expectedRepo: "/repo", - expectedRem: "/a", - }, - { - name: "Invalid URL format", - rawURL: "://invalid", - expectError: true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - owner, repo, rem, query, err := extractParts(tc.rawURL) - - if (err != nil) != tc.expectError { - t.Fatalf("extractParts() error = %v, expectError %v", err, tc.expectError) - } - - if !tc.expectError { - if owner != tc.expectedOwner { - t.Errorf("owner: got %q, want %q", owner, tc.expectedOwner) - } - if repo != tc.expectedRepo { - t.Errorf("repo: got %q, want %q", repo, tc.expectedRepo) - } - if rem != tc.expectedRem { - t.Errorf("remaining path: got %q, want %q", rem, tc.expectedRem) - } - if !reflect.DeepEqual(query, tc.expectedQuery) { - t.Errorf("query: got %v, want %v", query, tc.expectedQuery) - } - } - }) - } -} - -func TestMatchString_Compatibility(t *testing.T) { - testCases := []struct { - target string - expected bool - }{ - {"blob", true}, {"raw", true}, {"gist", true}, - {"clone", false}, {"releases", false}, - } - for _, tc := range testCases { - t.Run(tc.target, func(t *testing.T) { - if got := matchString(tc.target); got != tc.expected { - t.Errorf("matchString('%s') = %v; want %v", tc.target, got, tc.expected) - } - }) - } -} - -func BenchmarkMatcher(b *testing.B) { - cfg := &config.Config{} - path := "https://github.com/WJQSERVER/speedtest-ex/releases/download/v1.2.0/speedtest-linux-amd64.tar.gz" - - b.ReportAllocs() - b.ResetTimer() - - for i := 0; i < b.N; i++ { - _, _, _, _ = Matcher(path, cfg) - } -}