package attach import ( "slices" "strings" ) // sniffedExtensions maps a specific sniffed content type to the extensions // that may name it on disk; the first entry is the canonical one. The table // is fixed in code on purpose: package mime's answers depend on the host's // mime tables. var sniffedExtensions = map[string][]string{ "image/png": {"png"}, "image/jpeg": {"jpg", "jpeg", "jpe"}, "image/gif": {"gif"}, "image/webp": {"webp"}, "image/bmp": {"bmp"}, "image/x-icon": {"ico"}, "image/vnd.microsoft.icon": {"ico"}, "application/pdf": {"pdf"}, "audio/mpeg": {"mp3"}, "audio/wave": {"wav"}, "audio/wav": {"wav"}, "application/ogg": {"ogg", "oga", "opus"}, "audio/ogg": {"ogg", "oga", "opus"}, "video/webm": {"webm", "mkv"}, "video/mp4": {"mp4", "m4v"}, "font/woff2": {"woff2"}, "font/woff": {"woff"}, "font/ttf": {"ttf"}, "font/otf": {"otf"}, "application/vnd.ms-fontobject": {"eot"}, "application/x-rar-compressed": {"rar"}, "application/x-gzip": {"gz", "tgz"}, } // scriptCapableExtensions are extensions a browser or web server may run or // render as a document. They are never kept on disk under a generic sniff. var scriptCapableExtensions = []string{ "html", "htm", "xhtml", "xht", "shtml", "svg", "svgz", "xml", "xsl", "xslt", "js", "mjs", "css", "swf", "php", "phtml", "pht", "phar", } // DiskExtension returns the extension, without the dot, under which an // upload is stored, so a web server that picks the Content-Type from the // extension serves what the bytes are rather than what the client named // them. contentType is the sniffed base media type (http.DetectContentType // without parameters) and clientExt the client's allow-listed extension. // // For a specific sniffed type (PNG, JPEG, GIF, WebP, PDF, MP3, WebM, fonts // and so on) the client extension is kept when it belongs to that type, // else the type's canonical extension is used: a PNG named x.html is stored // as .png. Any other type is generic (application/octet-stream, // application/zip, text/plain, text/html, text/xml): the client extension // is kept, so .docx, .xlsx and .ics survive, unless it is script-capable // (html, svg, xml, js, css, php and the like), which becomes txt for a // text/* type and bin otherwise. func DiskExtension(contentType, clientExt string) string { ct := strings.ToLower(strings.TrimSpace(contentType)) if base, _, ok := strings.Cut(ct, ";"); ok { ct = strings.TrimSpace(base) } ext := strings.ToLower(strings.TrimPrefix(strings.TrimSpace(clientExt), ".")) if exts, ok := sniffedExtensions[ct]; ok { if slices.Contains(exts, ext) { return ext } return exts[0] } if extPattern.MatchString(ext) && !slices.Contains(scriptCapableExtensions, ext) { return ext } if strings.HasPrefix(ct, "text/") { return "txt" } return "bin" }