fix: use PathUnescape for filename URL decoding (#1214)

This commit is contained in:
Copilot authored and GitHub committed 2026-01-05 21:46:50 +08:00
1 parent 71c9e6947a
commit 6756d82ffa
5 files changed
+98 -6

No files matched your search

+8 -6
View File
@@ -171,9 +171,9 @@ func (f *Fetcher) Resolve(req *base.Request) error {
// get file filePath by URL
if file.Name == "" {
file.Name = path.Base(httpReq.URL.Path)
// Url decode
// Url decode - use PathUnescape for paths to handle %2B correctly
if file.Name != "" {
file.Name, _ = url.QueryUnescape(file.Name)
file.Name, _ = url.PathUnescape(file.Name)
}
}
// unknown file filePath
@@ -656,7 +656,8 @@ func parseFilenameExtended(cd string) string {
if len(parts) == 2 {
// parts[0] is charset (e.g., "UTF-8")
// parts[1] is percent-encoded value
decoded, err := url.QueryUnescape(parts[1])
// Use PathUnescape to handle %2B correctly (should decode to +, not space)
decoded, err := url.PathUnescape(parts[1])
if err == nil {
return decoded
}
@@ -665,7 +666,8 @@ func parseFilenameExtended(cd string) string {
// Try with single quote delimiter as well (some servers use this)
parts = strings.SplitN(value, "'", 3)
if len(parts) >= 3 {
decoded, err := url.QueryUnescape(parts[2])
// Use PathUnescape to handle %2B correctly (should decode to +, not space)
decoded, err := url.PathUnescape(parts[2])
if err == nil {
return decoded
}
@@ -687,8 +689,8 @@ func decodeFilenameParam(filename string) string {
}
}
// Try URL decoding
decoded := util.TryUrlQueryUnescape(filename)
// Try URL decoding - use PathUnescape for filenames to handle %2B correctly
decoded := util.TryUrlPathUnescape(filename)
// Check if the result is valid UTF-8. If not, try GBK decoding.
// This handles the case where Chinese Windows servers send GBK-encoded filenames
+26
View File
@@ -113,6 +113,32 @@ func TestFetcher_Resolve(t *testing.T) {
},
},
}, t)
// Test filename with plus signs (e.g., C++ Primer)
// Before fix: %2B decoded to space -> "C++ Primer" became "C Primer"
// After fix: %2B correctly decoded to + -> "C++ Primer Plus.mobi"
testResolve(test.StartTestCustomServer, "plus-sign-encoded", &base.Resource{
Size: test.BuildSize,
Range: false,
Files: []*base.FileInfo{
{
Name: "C++ Primer Plus.mobi",
Size: test.BuildSize,
},
},
}, t)
// Test filename with plus sign in URL path
// Before fix: %2B decoded to space
// After fix: %2B correctly decoded to +
testResolve(test.StartTestCustomServer, "C%2B%2B%20Primer.txt", &base.Resource{
Size: 0,
Range: false,
Files: []*base.FileInfo{
{
Name: "C++ Primer.txt",
Size: 0,
},
},
}, t)
// Test URL without file path - should use domain/host as filename
listener := test.StartTestRootServer()
+23
View File
@@ -169,6 +169,29 @@ func StartTestCustomServer() net.Listener {
defer file.Close()
io.Copy(writer, file)
})
// Test endpoint for filenames with plus signs (C++ files, etc.)
// This tests that %2B decodes to + not space
mux.HandleFunc("/plus-sign-encoded", func(writer http.ResponseWriter, request *http.Request) {
// Use filename*= format with %2B encoding for plus signs
writer.Header().Set("Content-Disposition", `attachment; filename*=UTF-8''C%2B%2B%20%20Primer%20%20Plus.mobi`)
writer.Header().Set("Content-Type", "application/octet-stream")
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
file, err := os.Open(BuildFile)
if err != nil {
panic(err)
}
defer file.Close()
io.Copy(writer, file)
})
// Test endpoint for plus sign in URL path
mux.HandleFunc("/C%2B%2B%20Primer.txt", func(writer http.ResponseWriter, request *http.Request) {
file, err := os.Open(BuildFile)
if err != nil {
panic(err)
}
defer file.Close()
io.Copy(writer, file)
})
return mux
})
}
+13
View File
@@ -66,3 +66,16 @@ func TryUrlQueryUnescape(s string) string {
}
return s
}
// TryUrlPathUnescape tries to unescape a URL path-encoded string.
// Unlike QueryUnescape, PathUnescape does not treat '+' as a space.
// This is the correct function to use for decoding URL paths and filenames
// where %2B should decode to '+', not to a space.
//
// If unescaping fails, it returns the original string.
func TryUrlPathUnescape(s string) string {
if decoded, err := url.PathUnescape(s); err == nil {
return decoded
}
return s
}
+28
View File
@@ -149,3 +149,31 @@ func TestTryURLDecode(t *testing.T) {
})
}
}
func TestTryUrlPathUnescape(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"normal.txt", "normal.txt"},
{"%E7%8F%80%E5%B0%94%E8%AF%BA.zip", "珀尔诺.zip"},
{"hello%20world.txt", "hello world.txt"},
{"bad%2-text", "bad%2-text"},
// The key difference: %2B should decode to + (not space)
{"C%2B%2B%20Primer.txt", "C++ Primer.txt"},
{"test%2Bfile.txt", "test+file.txt"},
// Plus sign in path should remain as-is (not decoded to space)
{"test+plus.txt", "test+plus.txt"},
// Mixed encoding
{"C%2B%2B%20%20Primer%20%20Plus.mobi", "C++ Primer Plus.mobi"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := TryUrlPathUnescape(tt.input)
if got != tt.expected {
t.Errorf("TryUrlPathUnescape(%q) = %q, want %q", tt.input, got, tt.expected)
}
})
}
}