diff --git a/internal/rpm/spectool/spectool.go b/internal/rpm/spectool/spectool.go index a807b1c4..09462867 100644 --- a/internal/rpm/spectool/spectool.go +++ b/internal/rpm/spectool/spectool.go @@ -13,12 +13,25 @@ import ( // filenameFromURL attempts to parse value as a URL and extract the basename. // Returns the filename and true if value is a URL, or ("", false) if not. +// +// RPM specs use fragment conventions like "#/local-name" and "#./local-name" +// to specify the local filename for URLs whose path doesn't contain a +// meaningful basename (e.g., keyserver lookup URLs). When a fragment starting +// with "/" or "./" is present, its basename is used instead of the URL path's +// basename. func filenameFromURL(value string) (string, bool) { parsed, err := url.Parse(value) if err != nil || parsed.Scheme == "" || parsed.Host == "" { return "", false } + // RPM "#/filename" convention: fragment overrides the path basename. + // Variants: "#/name.asc" and "#./name.asc" both occur in the wild. + // Plain anchors like "#section" are not filename overrides. + if strings.HasPrefix(parsed.Fragment, "/") || strings.HasPrefix(parsed.Fragment, "./") { + return path.Base(parsed.Fragment), true + } + return path.Base(parsed.Path), true } diff --git a/internal/rpm/spectool/spectool_test.go b/internal/rpm/spectool/spectool_test.go index 2800457e..1d52f521 100644 --- a/internal/rpm/spectool/spectool_test.go +++ b/internal/rpm/spectool/spectool_test.go @@ -73,6 +73,21 @@ func TestParseSpectoolOutput(t *testing.T) { input: "Source0: good.tar.gz\nPatch0: /bad/path\nPatch1: ok.patch", expected: []string{"good.tar.gz", "ok.patch"}, }, + { + name: "URL with slash fragment extracts fragment basename", + input: "Source0: https://example.com/lookup?q=abc#/local-name.asc", + expected: []string{"local-name.asc"}, + }, + { + name: "URL with dot-slash fragment extracts fragment basename", + input: "Source0: https://example.com/lookup?q=abc#./local-name.asc", + expected: []string{"local-name.asc"}, + }, + { + name: "URL with fragment rename overrides path basename", + input: "Patch0: https://example.com/pull/33.patch#/renamed.diff", + expected: []string{"renamed.diff"}, + }, } for _, testCase := range tests { @@ -96,7 +111,9 @@ func TestFilenameFromURL(t *testing.T) { }{ {"https URL", "https://example.com/file.tar.gz", "file.tar.gz", true}, {"URL with query", "https://example.com/file.tar.gz?raw=true", "file.tar.gz", true}, - {"URL with fragment", "https://example.com/file.tar.gz#section", "file.tar.gz", true}, + {"URL with non-path fragment", "https://example.com/file.tar.gz#section", "file.tar.gz", true}, + {"RPM fragment slash", "https://example.com/lookup?q=abc#/local-name.asc", "local-name.asc", true}, + {"RPM fragment dot-slash", "https://example.com/lookup?q=abc#./local-name.asc", "local-name.asc", true}, {"nested URL path", "https://example.com/a/b/c/file.tar.gz", "file.tar.gz", true}, {"ftp URL", "ftp://ftp.gnu.org/pub/gnu/sed/sed-4.9.tar.xz", "sed-4.9.tar.xz", true}, {"trailing slash", "https://example.com/", "/", true},