Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions internal/rpm/spectool/spectool.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
19 changes: 18 additions & 1 deletion internal/rpm/spectool/spectool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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},
Expand Down
Loading