detached 2cb614b

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 {
4646 return clean
4747 }
4848
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+
4965 // GetExtensionFromMimeType returns a file extension for a given MIME type.
5066 // Returns empty string if MIME type is unknown or empty.
5167 // Handles MIME types with parameters (e.g., "application/pdf; charset=binary").
@@ -61,6 +77,10 @@ func GetExtensionFromMimeType(mimeType string) string {
6177 return ""
6278 }
6379
80+ if ext, ok := preferredExtensions[mediaType]; ok {
81+ return ext
82+ }
83+
6484 exts, err := mime.ExtensionsByType(mediaType)
6585 if err != nil || len(exts) == 0 {
6686 return ""
@@ -46,6 +46,22 @@ func SanitizeFilenameWithExtension(name, mimeType string) string {
46 return clean46 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) {
147147 {
148148 name: "text html with charset",
149149 input: "text/html; charset=utf-8",
150- expected: ".htm",
150+ expected: ".html",
151151 },
152152 {
153153 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",

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close