返回 DeepSeek-Reasonix
validate.go
根目录 / internal / attachment / validate.go
1 package attachment
2
3 import (
4 "bytes"
5 "image"
6 _ "image/gif"
7 _ "image/jpeg"
8 _ "image/png"
9 "strings"
10 "unicode"
11 "unicode/utf8"
12
13 _ "golang.org/x/image/webp"
14 )
15
16 var sniffHeaders = []struct {
17 mime string
18 prefix []byte
19 }{
20 {"image/png", []byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'}},
21 {"image/gif", []byte("GIF87a")},
22 {"image/gif", []byte("GIF89a")},
23 {"image/webp", []byte("RIFF")},
24 {"image/jpeg", []byte{0xff, 0xd8, 0xff}},
25 }
26
27 func DetectMIME(raw []byte) string {
28 for _, h := range sniffHeaders {
29 if !bytes.HasPrefix(raw, h.prefix) {
30 continue
31 }
32 if h.mime == "image/webp" && (len(raw) < 12 || string(raw[8:12]) != "WEBP") {
33 continue
34 }
35 return h.mime
36 }
37 return ""
38 }
39
40 func NormalizeDisplayName(name string) string {
41 name = strings.ReplaceAll(name, "\\", "/")
42 if i := strings.LastIndex(name, "/"); i >= 0 {
43 name = name[i+1:]
44 }
45 var b strings.Builder
46 for _, r := range name {
47 if r < 32 || r == 127 || !utf8.ValidRune(r) || unicode.Is(unicode.C, r) {
48 continue
49 }
50 b.WriteRune(r)
51 }
52 out := strings.TrimSpace(b.String())
53 if out == "" {
54 return "image"
55 }
56 return out
57 }
58
59 type verifiedImage struct {
60 MIME string
61 Width int
62 Height int
63 Bytes []byte
64 }
65
66 func ValidateImage(raw []byte, declaredMIME string, policy Policy) (mime string, width, height int, err error) {
67 verified, err := verifyImageBytes(raw, declaredMIME, policy)
68 if err != nil {
69 return "", 0, 0, err
70 }
71 return verified.MIME, verified.Width, verified.Height, nil
72 }
73
74 func verifyImageBytes(raw []byte, declaredMIME string, policy Policy) (verifiedImage, error) {
75 policy = policy.withDefaults()
76 if len(raw) == 0 || int64(len(raw)) > policy.MaxBytes {
77 return verifiedImage{}, Error{Code: CodeSize, Message: defaultDetail(CodeSize)}
78 }
79 mime := DetectMIME(raw)
80 if mime == "" {
81 return verifiedImage{}, Error{Code: CodeUnsupported, Message: defaultDetail(CodeUnsupported)}
82 }
83 if declared := normalizeDeclaredMIME(declaredMIME); declared != "" && declared != mime {
84 return verifiedImage{}, Error{Code: CodeUnsupported, Message: "declared MIME does not match image bytes"}
85 }
86 cfg, format, err := image.DecodeConfig(bytes.NewReader(raw))
87 if err != nil {
88 return verifiedImage{}, Error{Code: CodeCorrupt, Message: defaultDetail(CodeCorrupt), Cause: err}
89 }
90 if imageMIME(format) != mime {
91 return verifiedImage{}, Error{Code: CodeUnsupported, Message: "decoded format does not match image bytes"}
92 }
93 if cfg.Width <= 0 || cfg.Height <= 0 {
94 return verifiedImage{}, Error{Code: CodeCorrupt, Message: defaultDetail(CodeCorrupt)}
95 }
96 if int64(cfg.Width)*int64(cfg.Height) > policy.MaxPixels {
97 return verifiedImage{}, Error{Code: CodeSize, Message: "exceeds the allowed pixel count"}
98 }
99 decoded, decodedFormat, err := image.Decode(bytes.NewReader(raw))
100 if err != nil || decoded == nil {
101 return verifiedImage{}, Error{Code: CodeCorrupt, Message: defaultDetail(CodeCorrupt), Cause: err}
102 }
103 bounds := decoded.Bounds()
104 if imageMIME(decodedFormat) != mime || bounds.Dx() <= 0 || bounds.Dy() <= 0 {
105 return verifiedImage{}, Error{Code: CodeCorrupt, Message: defaultDetail(CodeCorrupt)}
106 }
107 return verifiedImage{MIME: mime, Width: cfg.Width, Height: cfg.Height, Bytes: append([]byte(nil), raw...)}, nil
108 }
109
110 func normalizeDeclaredMIME(mime string) string {
111 mime = strings.ToLower(strings.TrimSpace(strings.SplitN(mime, ";", 2)[0]))
112 switch mime {
113 case "image/png", "image/jpeg", "image/gif", "image/webp":
114 return mime
115 case "image/jpg":
116 return "image/jpeg"
117 default:
118 return ""
119 }
120 }
121
122 func imageMIME(format string) string {
123 switch format {
124 case "png":
125 return "image/png"
126 case "jpeg":
127 return "image/jpeg"
128 case "gif":
129 return "image/gif"
130 case "webp":
131 return "image/webp"
132 default:
133 return ""
134 }
135 }
136
137 //nolint:unused // The final variant layer reuses these validation semantics.
138 func hasAlpha(img image.Image) bool {
139 switch img.(type) {
140 case *image.NRGBA, *image.NRGBA64, *image.RGBA, *image.RGBA64, *image.Alpha, *image.Alpha16:
141 return true
142 }
143 if img == nil {
144 return false
145 }
146 b := img.Bounds()
147 for y := b.Min.Y; y < b.Max.Y; y++ {
148 for x := b.Min.X; x < b.Max.X; x++ {
149 _, _, _, a := img.At(x, y).RGBA()
150 if a != 0xffff {
151 return true
152 }
153 }
154 }
155 return false
156 }
157
157 lines GO