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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
string("steAm://%2F%3F")
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
string("http://?#?#%")
24 changes: 15 additions & 9 deletions pkg/helpers/uris.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,13 @@ func DecodeURIIfNeeded(uri string) string {
// slash structure while decoding percent-encoded characters.
rest := strings.TrimRight(parsed.Rest, "/")
segments := strings.Split(rest, "/")
// Re-encoder for gen-delims that would change URI structure on a
// second parse pass: / (path sep), ? (query), # (fragment).
reenc := strings.NewReplacer("/", "%2F", "?", "%3F", "#", "%23")
for i, seg := range segments {
decoded, err := url.PathUnescape(seg)
if err == nil && utf8.ValidString(decoded) {
// Re-encode any slashes from %2F so they don't become
// structural separators on subsequent passes
segments[i] = strings.ReplaceAll(decoded, "/", "%2F")
segments[i] = reenc.Replace(decoded)
}
}
reconstructed := parsed.Scheme + "://" + strings.Join(segments, "/")
Expand All @@ -95,18 +96,23 @@ func DecodeURIIfNeeded(uri string) string {

// Handle standard web schemes (http/https)
if shared.IsStandardSchemeForDecoding(schemeLower) {
// Extract fragment from query if present (only for http/https)
// Per RFC 3986, '#' introduces the fragment and takes precedence over
// '?', which ParseURIComponents picks up as the query separator.
// Extract fragment from the raw URI first so that a fragment containing
// '?' doesn't shift on a second parse pass (idempotence).
var fragment string
query := parsed.Query
if idx := strings.Index(query, "#"); idx >= 0 {
fragment = query[idx+1:]
query = query[:idx]
fragURI := uri
if idx := strings.Index(uri, "#"); idx >= 0 {
fragment = uri[idx+1:]
fragURI = uri[:idx]
}
hParsed := virtualpath.ParseURIComponents(fragURI)
query := hParsed.Query

// Split rest into userinfo@host and path
// Format: [userinfo@]host/path
var userinfo, host, pathPart string
rest := parsed.Rest
rest := hParsed.Rest

// Check for userinfo (use LastIndex to handle @ in passwords)
if idx := strings.LastIndex(rest, "@"); idx >= 0 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/helpers/uris_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ func TestDecodeURIIfNeeded_EdgeCases(t *testing.T) {
{
name: "kodi_with_fragment",
input: "kodi-movie://456/The%20Matrix#play",
expected: "kodi-movie://456/The Matrix#play", // Fragment kept as part of name
expected: "kodi-movie://456/The Matrix%23play", // '#' re-encoded to keep it in path, not fragment
},
{
name: "http_with_query_and_fragment",
Expand Down
Loading