fix(server): handle dot-only names and long fake extensions

A name made only of dots trimmed down to an empty filename. It now
falls back to download, like an empty stem does.

path.Ext treats anything after the last dot as the extension, so a long
suffix with no real extension was kept whole and replaced the stem with
download, going past the 255-byte cap. Suffixes longer than 16 bytes
are now treated as part of the stem and truncated with it.

Neither case is reachable from the current call sites, which always
append a short extension.
This commit is contained in:
Deluan 2026-09-23 09:34:38 -04:00
parent 86890feb77
commit e4813fe053
2 changed files with 21 additions and 4 deletions

View File

@ -12,7 +12,11 @@ import (
"github.com/deluan/sanitize"
)
const maxFilenameBytes = 255
const (
maxFilenameBytes = 255
maxExtensionBytes = 16
fallbackFilename = "download"
)
// ContentDispositionAttachment builds an RFC 6266 attachment header value for a user-controlled filename.
// Non-ASCII names also get a filename* parameter, which clients prefer over the ASCII fallback.
@ -36,8 +40,11 @@ func splitFilename(filename string) (stem, ext string) {
return r
}, SanitizeFilename(strings.ToValidUTF8(filename, "_")))
ext = path.Ext(name)
if len(ext) > maxExtensionBytes {
ext = ""
}
stem = strings.TrimSuffix(name, ext)
if limit := max(maxFilenameBytes-len(ext), 0); len(stem) > limit {
if limit := maxFilenameBytes - len(ext); len(stem) > limit {
for limit > 0 && !utf8.RuneStart(stem[limit]) {
limit--
}
@ -47,8 +54,8 @@ func splitFilename(filename string) (stem, ext string) {
}
func joinFilename(stem, ext string) string {
stem = cmp.Or(strings.TrimSpace(stem), "download")
return strings.TrimRight(stem+ext, " .")
stem = cmp.Or(strings.TrimSpace(stem), fallbackFilename)
return cmp.Or(strings.TrimRight(stem+ext, " ."), fallbackFilename)
}
func toASCII(s string) string {

View File

@ -61,6 +61,16 @@ var _ = Describe("ContentDispositionAttachment", func() {
Expect(asciiName("Loose End. ")).To(Equal("Loose End"))
})
It("falls back to a placeholder when only dots are left", func() {
Expect(str.ContentDispositionAttachment("...")).To(Equal(`attachment; filename="download"`))
})
It("caps a long name whose last dot is not an extension", func() {
name := asciiName("a." + strings.Repeat("x", 300))
Expect(len(name)).To(BeNumerically("<=", 255))
Expect(name).To(HavePrefix("a.xxx"))
})
It("replaces path separators and reserved characters", func() {
Expect(asciiName("AC/DC: Live, 1979?.zip")).To(Equal("AC_DC_ Live_ 1979_.zip"))
})