diff --git a/pkg/download/extension_match_test.go b/pkg/download/extension_match_test.go new file mode 100644 index 00000000..185aad67 --- /dev/null +++ b/pkg/download/extension_match_test.go @@ -0,0 +1,56 @@ +package download + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/GopeedLab/gopeed/pkg/base" +) + +func TestDownloader_Extension_MagnetMatch(t *testing.T) { + const magnetURL = "magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef01234567" + for _, pattern := range []string{"magnet:*", "magnet:?*"} { + t.Run(pattern, func(t *testing.T) { + d := NewDownloader(&DownloaderConfig{ + Storage: NewMemStorage(), + StorageDir: t.TempDir(), + TempDir: t.TempDir(), + }) + if err := d.Setup(); err != nil { + t.Fatal(err) + } + defer d.Clear() + + extDir := t.TempDir() + manifest := fmt.Sprintf(`{"name":"magnet-match","author":"test","title":"Magnet match","version":"1.0.0","scripts":[{"event":"onResolve","match":{"urls":[%q]},"entry":"index.js"}]}`, pattern) + script := `gopeed.events.onResolve(ctx => { + ctx.res = {files: [{name: "resolved.txt", size: 1, req: {url: "https://example.com/resolved.txt"}}]}; +});` + for name, content := range map[string]string{"manifest.json": manifest, "index.js": script} { + if err := os.WriteFile(filepath.Join(extDir, name), []byte(content), 0600); err != nil { + t.Fatal(err) + } + } + if _, err := d.InstallExtensionByFolder(extDir, true); err != nil { + t.Fatal(err) + } + + for _, req := range []*base.Request{ + {URL: magnetURL}, + {URL: "https://example.com/rewritten", RawURL: magnetURL}, + } { + // Exercise extension activation without falling back to a live + // BitTorrent metadata lookup when the pattern fails to match. + res, err := d.triggerOnResolve(req) + if err != nil { + t.Fatal(err) + } + if res == nil || len(res.Files) != 1 || res.Files[0].Name != "resolved.txt" { + t.Fatalf("magnet extension did not resolve request %+v: %+v", req, res) + } + } + }) + } +} diff --git a/pkg/util/matcher.go b/pkg/util/matcher.go index 02f609c9..23fcc572 100644 --- a/pkg/util/matcher.go +++ b/pkg/util/matcher.go @@ -6,14 +6,27 @@ import ( "strings" ) -// Match url with pattern by chrome extension match pattern style +// Match url with pattern by chrome extension match pattern style. +// Magnet URI patterns such as magnet:?* are also supported, with * as a wildcard. // https://developer.chrome.com/docs/extensions/mv3/match_patterns/ func Match(pattern string, u string) bool { - scheme, host, path := parsePattern(pattern) url, err := url.Parse(u) if err != nil { return false } + // Magnet URIs have no authority or path; match the raw scheme-specific + // part instead, preserving query punctuation and percent-encoding. + if strings.HasPrefix(pattern, "magnet:") && !strings.HasPrefix(pattern, "magnet://") { + if url.Scheme != "magnet" { + return false + } + _, target, _ := strings.Cut(u, ":") + expression := regexp.QuoteMeta(strings.TrimPrefix(pattern, "magnet:")) + expression = strings.ReplaceAll(expression, `\*`, ".*") + matched, _ := regexp.MatchString("^"+expression+"$", target) + return matched + } + scheme, host, path := parsePattern(pattern) if scheme != "*" && scheme != url.Scheme { return false } diff --git a/pkg/util/matcher_test.go b/pkg/util/matcher_test.go index 5f24442e..0635d94d 100644 --- a/pkg/util/matcher_test.go +++ b/pkg/util/matcher_test.go @@ -34,3 +34,35 @@ func TestMatch(t *testing.T) { }) } } + +func TestMatchMagnet(t *testing.T) { + const magnetURL = "magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef01234567&dn=example.zip" + tests := []struct { + name string + pattern string + url string + want bool + }{ + {"scheme wildcard", "magnet:*", magnetURL, true}, + {"query wildcard", "magnet:?*", magnetURL, true}, + {"info hash wildcard", "magnet:?xt=urn:btih:*", magnetURL, true}, + {"exact URI", magnetURL, magnetURL, true}, + {"encoded name", "magnet:?*&dn=example%20file.zip", "magnet:?xt=urn:btih:abc&dn=example%20file.zip", true}, + {"literal query punctuation", "magnet:?*&dn=a+b[1].zip", "magnet:?xt=urn:btih:abc&dn=a+b[1].zip", true}, + {"literal question mark", "magnet:?*", "magnet:xt=urn:btih:abc", false}, + {"literal dot", "magnet:?*&dn=example.zip", "magnet:?xt=urn:btih:abc&dn=exampleXzip", false}, + {"different hash type", "magnet:?xt=urn:btmh:*", magnetURL, false}, + {"different hash", "magnet:?xt=urn:btih:other*", magnetURL, false}, + {"different scheme", "magnet:*", "https://example.org/file.zip", false}, + {"invalid URI", "magnet:*", "magnet:\x00", false}, + {"existing catch-all", "*://*/*", magnetURL, true}, + {"existing magnet pattern", "magnet://*", magnetURL, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Match(tt.pattern, tt.url); got != tt.want { + t.Errorf("Match(%q, %q) = %v, want %v", tt.pattern, tt.url, got, tt.want) + } + }) + } +}