mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-26 12:35:35 +02:00
Preserve non-ASCII attachment filenames with RFC 5987 while retaining a safe ASCII fallback.
136 lines
4.6 KiB
Go
136 lines
4.6 KiB
Go
package storage
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestIsInlineContentType(t *testing.T) {
|
|
cases := []struct {
|
|
ct string
|
|
want bool
|
|
}{
|
|
{"image/png", true},
|
|
{"image/jpeg", true},
|
|
{"image/gif", true},
|
|
{"image/webp", true},
|
|
{"video/mp4", true},
|
|
{"audio/mpeg", true},
|
|
{"application/pdf", true},
|
|
|
|
// SVG must NOT render inline — it can carry executable script.
|
|
{"image/svg+xml", false},
|
|
// MIME types are case-insensitive (RFC 2045 §5.1) and may carry
|
|
// parameters. The SVG carve-out is a security boundary, so any
|
|
// variant that resolves to image/svg+xml must also be blocked.
|
|
{"IMAGE/SVG+XML", false},
|
|
{"Image/Svg+Xml", false},
|
|
{"image/svg+xml; charset=utf-8", false},
|
|
{"image/svg+xml;charset=utf-8", false},
|
|
{" image/svg+xml ", false},
|
|
// Normalization must not break the positive cases either.
|
|
{"IMAGE/PNG", true},
|
|
{"image/png; foo=bar", true},
|
|
{" application/pdf", true},
|
|
|
|
{"text/html", false},
|
|
{"application/octet-stream", false},
|
|
{"text/plain", false},
|
|
{"", false},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := isInlineContentType(tc.ct); got != tc.want {
|
|
t.Errorf("isInlineContentType(%q) = %v, want %v", tc.ct, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestContentDisposition(t *testing.T) {
|
|
if got := ContentDisposition("image/png", `nice"file;.png`); got != `inline; filename="nice_file_.png"` {
|
|
t.Fatalf("ContentDisposition image = %q", got)
|
|
}
|
|
if got := ContentDisposition("text/plain", "notes.txt"); got != `attachment; filename="notes.txt"` {
|
|
t.Fatalf("ContentDisposition text = %q", got)
|
|
}
|
|
if got := ContentDisposition("image/svg+xml", "logo.svg"); got != `attachment; filename="logo.svg"` {
|
|
t.Fatalf("ContentDisposition svg = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestContentDispositionNonASCII(t *testing.T) {
|
|
// Chinese filename — should include filename* and an ASCII-only fallback.
|
|
got := ContentDisposition("image/webp", "微信图片_2026-04-09_162004_785.webp")
|
|
if !strings.Contains(got, "filename*=UTF-8''") {
|
|
t.Fatalf("ContentDisposition should include filename* for non-ASCII: %q", got)
|
|
}
|
|
const legacyPrefix = `filename="`
|
|
start := strings.Index(got, legacyPrefix)
|
|
if start == -1 {
|
|
t.Fatalf("ContentDisposition should keep ASCII fallback: %q", got)
|
|
}
|
|
// Legacy filename parameter must be ASCII only.
|
|
start += len(legacyPrefix)
|
|
end := strings.IndexByte(got[start:], '"')
|
|
if end == -1 {
|
|
t.Fatalf("ContentDisposition fallback is not terminated: %q", got)
|
|
}
|
|
fallback := got[start : start+end]
|
|
for _, r := range fallback {
|
|
if r > 0x7f {
|
|
t.Fatalf("legacy filename parameter must be ASCII only, got: %q", fallback)
|
|
}
|
|
}
|
|
// Modern filename* must contain the percent-encoded original name.
|
|
if !strings.Contains(got, "%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87") {
|
|
t.Fatalf("filename* should encode the original Chinese name: %q", got)
|
|
}
|
|
|
|
// ASCII-only filename — no filename*
|
|
got2 := ContentDisposition("text/plain", "notes.txt")
|
|
if strings.Contains(got2, "filename*=") {
|
|
t.Fatalf("ContentDisposition should NOT include filename* for ASCII: %q", got2)
|
|
}
|
|
|
|
// Mixed ASCII + special chars (but no non-ASCII) — no filename*
|
|
got3 := ContentDisposition("image/png", `nice"file;.png`)
|
|
if strings.Contains(got3, "filename*=") {
|
|
t.Fatalf("ContentDisposition should NOT include filename* for pure ASCII with special chars: %q", got3)
|
|
}
|
|
}
|
|
|
|
func TestAttachmentContentDispositionNonASCII(t *testing.T) {
|
|
got := AttachmentContentDisposition("微信图片_2026-04-09_162004_785.webp")
|
|
wantPrefix := `attachment; filename="_____2026-04-09_162004_785.webp"; filename*=UTF-8''`
|
|
if !strings.HasPrefix(got, wantPrefix) {
|
|
t.Fatalf("AttachmentContentDisposition = %q, want prefix %q", got, wantPrefix)
|
|
}
|
|
if !strings.Contains(got, "%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87") {
|
|
t.Fatalf("AttachmentContentDisposition should encode the original Chinese name: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestRFC5987Encode(t *testing.T) {
|
|
got := rfc5987Encode("微信图片")
|
|
want := "%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87"
|
|
if got != want {
|
|
t.Fatalf("rfc5987Encode(%q) = %q, want %q", "微信图片", got, want)
|
|
}
|
|
// ASCII pass-through
|
|
if got2 := rfc5987Encode("hello.txt"); got2 != "hello.txt" {
|
|
t.Fatalf("rfc5987Encode(ASCII) = %q, want %q", got2, "hello.txt")
|
|
}
|
|
// Space and special chars are encoded
|
|
if got3 := rfc5987Encode("a b"); got3 != "a%20b" {
|
|
t.Fatalf("rfc5987Encode(space) = %q, want %q", got3, "a%20b")
|
|
}
|
|
}
|
|
|
|
func TestNeedsRFC5987Encoding(t *testing.T) {
|
|
if needsRFC5987Encoding("hello.txt") {
|
|
t.Fatal("ASCII filename should not need RFC 5987 encoding")
|
|
}
|
|
if !needsRFC5987Encoding("微信图片.webp") {
|
|
t.Fatal("Chinese filename should need RFC 5987 encoding")
|
|
}
|
|
}
|