fix(download): use preferred extensions map for platform-stable MIME lookups
mime.ExtensionsByType returns extensions in alphabetical order from the system MIME database, which varies by platform and Go version (e.g. .ehtml before .html for text/html). Add a preferredExtensions map for common MIME types to always return the canonical extension.
- commit
- 2cb614b9eb1eed1017cce97b5e7966350a5c9b0b
- author
- brice · · 2026-02-12 18:28 UTC
- parent
- 87e495c
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +21 -1
M internal/domain/download/filename.go +20 -0
| @@ -46,6 +46,22 @@ func SanitizeFilenameWithExtension(name, mimeType string) string { | |||
| 46 | 46 | return clean | |
| 47 | 47 | } | |
| 48 | 48 | ||
| 49 | + | // preferredExtensions maps MIME types whose stdlib extension list is | |
| 50 | + | // platform-dependent (alphabetical order) to the canonical extension. | |
| 51 | + | // Without this, mime.ExtensionsByType("text/html") may return ".ehtml" | |
| 52 | + | // instead of ".html" depending on the system MIME database. | |
| 53 | + | var preferredExtensions = map[string]string{ | |
| 54 | + | "text/html": ".html", | |
| 55 | + | "text/plain": ".txt", | |
| 56 | + | "text/xml": ".xml", | |
| 57 | + | "application/xhtml+xml": ".xhtml", | |
| 58 | + | "image/jpeg": ".jpg", | |
| 59 | + | "image/svg+xml": ".svg", | |
| 60 | + | "audio/mpeg": ".mp3", | |
| 61 | + | "video/mp4": ".mp4", | |
| 62 | + | "application/octet-stream": ".bin", | |
| 63 | + | } | |
| 64 | + | ||
| 49 | 65 | // GetExtensionFromMimeType returns a file extension for a given MIME type. | |
| 50 | 66 | // Returns empty string if MIME type is unknown or empty. | |
| 51 | 67 | // Handles MIME types with parameters (e.g., "application/pdf; charset=binary"). | |
| @@ -61,6 +77,10 @@ func GetExtensionFromMimeType(mimeType string) string { | |||
| 61 | 77 | return "" | |
| 62 | 78 | } | |
| 63 | 79 | ||
| 80 | + | if ext, ok := preferredExtensions[mediaType]; ok { | |
| 81 | + | return ext | |
| 82 | + | } | |
| 83 | + | ||
| 64 | 84 | exts, err := mime.ExtensionsByType(mediaType) | |
| 65 | 85 | if err != nil || len(exts) == 0 { | |
| 66 | 86 | return "" | |
| @@ -46,6 +46,22 @@ func SanitizeFilenameWithExtension(name, mimeType string) string { | |||||
| 46 | return clean | 46 | return clean | ||
| 47 | } | 47 | } | ||
| 48 | 48 | ||||
| 49 | + | // preferredExtensions maps MIME types whose stdlib extension list is | |||
| 50 | + | // platform-dependent (alphabetical order) to the canonical extension. | |||
| 51 | + | // Without this, mime.ExtensionsByType("text/html") may return ".ehtml" | |||
| 52 | + | // instead of ".html" depending on the system MIME database. | |||
| 53 | + | var preferredExtensions = map[string]string{ | |||
| 54 | + | "text/html": ".html", | |||
| 55 | + | "text/plain": ".txt", | |||
| 56 | + | "text/xml": ".xml", | |||
| 57 | + | "application/xhtml+xml": ".xhtml", | |||
| 58 | + | "image/jpeg": ".jpg", | |||
| 59 | + | "image/svg+xml": ".svg", | |||
| 60 | + | "audio/mpeg": ".mp3", | |||
| 61 | + | "video/mp4": ".mp4", | |||
| 62 | + | "application/octet-stream": ".bin", | |||
| 63 | + | } | |||
| 64 | + | ||||
| 49 | // GetExtensionFromMimeType returns a file extension for a given MIME type. | 65 | // GetExtensionFromMimeType returns a file extension for a given MIME type. | ||
| 50 | // Returns empty string if MIME type is unknown or empty. | 66 | // Returns empty string if MIME type is unknown or empty. | ||
| 51 | // Handles MIME types with parameters (e.g., "application/pdf; charset=binary"). | 67 | // Handles MIME types with parameters (e.g., "application/pdf; charset=binary"). | ||
| @@ -61,6 +77,10 @@ func GetExtensionFromMimeType(mimeType string) string { | |||||
| 61 | return "" | 77 | return "" | ||
| 62 | } | 78 | } | ||
| 63 | 79 | ||||
| 80 | + | if ext, ok := preferredExtensions[mediaType]; ok { | |||
| 81 | + | return ext | |||
| 82 | + | } | |||
| 83 | + | ||||
| 64 | exts, err := mime.ExtensionsByType(mediaType) | 84 | exts, err := mime.ExtensionsByType(mediaType) | ||
| 65 | if err != nil || len(exts) == 0 { | 85 | if err != nil || len(exts) == 0 { | ||
| 66 | return "" | 86 | return "" | ||
M internal/domain/download/filename_test.go +1 -1
| @@ -147,7 +147,7 @@ func TestGetExtensionFromMimeType(t *testing.T) { | |||
| 147 | 147 | { | |
| 148 | 148 | name: "text html with charset", | |
| 149 | 149 | input: "text/html; charset=utf-8", | |
| 150 | - | expected: ".htm", | |
| 150 | + | expected: ".html", | |
| 151 | 151 | }, | |
| 152 | 152 | { | |
| 153 | 153 | name: "mime type with multiple params", | |
| @@ -147,7 +147,7 @@ func TestGetExtensionFromMimeType(t *testing.T) { | |||||
| 147 | { | 147 | { | ||
| 148 | name: "text html with charset", | 148 | name: "text html with charset", | ||
| 149 | input: "text/html; charset=utf-8", | 149 | input: "text/html; charset=utf-8", | ||
| 150 | - | expected: ".htm", | 150 | + | expected: ".html", |
| 151 | }, | 151 | }, | ||
| 152 | { | 152 | { | ||
| 153 | name: "mime type with multiple params", | 153 | name: "mime type with multiple params", | ||