diff --git a/internal/protocol/http/filename_parse_test.go b/internal/protocol/http/filename_parse_test.go index 6945250f..742399cd 100644 --- a/internal/protocol/http/filename_parse_test.go +++ b/internal/protocol/http/filename_parse_test.go @@ -125,13 +125,26 @@ func TestUnescapeHTMLEntities(t *testing.T) { in: "normal.txt", want: "normal.txt", }, + { + name: "escaped ampersand before entity name is decoded once", + in: "a&lt;b.txt", + want: "a<b.txt", + }, + { + name: "escaped ampersand before quote entity is decoded once", + in: "a&quot;b.txt", + want: "a"b.txt", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := unescapeHTMLEntities(tt.in) - if got != tt.want { - t.Errorf("unescapeHTMLEntities() = %q, want %q", got, tt.want) + // Run repeatedly so that an order-dependent implementation fails reliably. + for i := 0; i < 200; i++ { + got := unescapeHTMLEntities(tt.in) + if got != tt.want { + t.Fatalf("unescapeHTMLEntities() = %q, want %q (run %d)", got, tt.want, i) + } } }) } diff --git a/internal/protocol/http/helper.go b/internal/protocol/http/helper.go index 30a27bac..5a1fa862 100644 --- a/internal/protocol/http/helper.go +++ b/internal/protocol/http/helper.go @@ -408,24 +408,21 @@ func decodeFilenameParam(filename string) string { return decoded } +// htmlEntityReplacer replaces every entity in a single left-to-right pass, so an +// escaped ampersand such as "&lt;" becomes "<" and is not decoded twice. +var htmlEntityReplacer = strings.NewReplacer( + "&", "&", + "<", "<", + ">", ">", + """, "\"", + "'", "'", + "'", "'", +) + // unescapeHTMLEntities unescapes common HTML entities in filenames // This handles cases where servers HTML-encode special characters like & to & func unescapeHTMLEntities(s string) string { - // Common HTML entities that might appear in filenames - replacements := map[string]string{ - "&": "&", - "<": "<", - ">": ">", - """: "\"", - "'": "'", - "'": "'", - } - - result := s - for entity, char := range replacements { - result = strings.ReplaceAll(result, entity, char) - } - return result + return htmlEntityReplacer.Replace(s) } // tryDecodeGBK attempts to decode string as GBK encoding