validate_test.go (6762B)
1 package ico 2 3 import ( 4 "bytes" 5 "encoding/binary" 6 "image" 7 "image/color" 8 "image/png" 9 "strings" 10 "testing" 11 ) 12 13 // frame is one icon to place in a file the encoder would not write itself. 14 type frame struct { 15 width, height int 16 data []byte 17 } 18 19 // buildICO assembles a file from payloads already encoded, so a test can 20 // store something invalid on purpose. 21 func buildICO(frames ...frame) []byte { 22 out := make([]byte, 0, directorySize+entrySize*len(frames)) 23 out = binary.LittleEndian.AppendUint16(out, 0) 24 out = binary.LittleEndian.AppendUint16(out, 1) 25 out = binary.LittleEndian.AppendUint16(out, uint16(len(frames))) 26 offset := directorySize + entrySize*len(frames) 27 for _, f := range frames { 28 out = append(out, byte(f.width), byte(f.height), 0, 0) 29 out = binary.LittleEndian.AppendUint16(out, 1) 30 out = binary.LittleEndian.AppendUint16(out, 32) 31 out = binary.LittleEndian.AppendUint32(out, uint32(len(f.data))) 32 out = binary.LittleEndian.AppendUint32(out, uint32(offset)) 33 offset += len(f.data) 34 } 35 for _, f := range frames { 36 out = append(out, f.data...) 37 } 38 return out 39 } 40 41 // encodePNG encodes img the way a program that does not know what Windows 42 // requires would: through image/png, which drops the alpha channel from an 43 // image that is entirely opaque. 44 func encodePNG(t *testing.T, img image.Image) []byte { 45 t.Helper() 46 var buf bytes.Buffer 47 if err := png.Encode(&buf, img); err != nil { 48 t.Fatalf("encoding png: %v", err) 49 } 50 return buf.Bytes() 51 } 52 53 func validate(t *testing.T, data []byte) []Problem { 54 t.Helper() 55 problems, err := Validate(bytes.NewReader(data)) 56 if err != nil { 57 t.Fatalf("validating: %v", err) 58 } 59 return problems 60 } 61 62 // find returns the first problem whose message contains want. 63 func find(problems []Problem, want string) (Problem, bool) { 64 for _, p := range problems { 65 if strings.Contains(p.Message, want) { 66 return p, true 67 } 68 } 69 return Problem{}, false 70 } 71 72 func TestValidateAcceptsOurOwnOutput(t *testing.T) { 73 var buf bytes.Buffer 74 if err := Encode(&buf, gradient(256)); err != nil { 75 t.Fatalf("encoding: %v", err) 76 } 77 if problems := validate(t, buf.Bytes()); len(problems) != 0 { 78 for _, p := range problems { 79 t.Errorf("unexpected problem: %s", p) 80 } 81 } 82 } 83 84 // TestValidateAcceptsAnOpaqueSource covers the case the encoder guards 85 // against: an image with no transparency still has to reach Windows with an 86 // alpha channel. 87 func TestValidateAcceptsAnOpaqueSource(t *testing.T) { 88 var buf bytes.Buffer 89 if err := Encode(&buf, solid(256, color.NRGBA{R: 1, G: 2, B: 3, A: 255})); err != nil { 90 t.Fatalf("encoding: %v", err) 91 } 92 if problems := validate(t, buf.Bytes()); len(problems) != 0 { 93 for _, p := range problems { 94 t.Errorf("unexpected problem: %s", p) 95 } 96 } 97 } 98 99 func TestValidateReportsAPNGWithoutAlpha(t *testing.T) { 100 // image/png writes truecolour for an image that is entirely opaque, 101 // which is the frame Windows passes over. 102 opaque := encodePNG(t, solid(256, color.NRGBA{R: 1, G: 2, B: 3, A: 255})) 103 if _, _, colour, ok := ihdr(opaque); !ok || colour != 2 { 104 t.Fatalf("fixture is colour type %d, want truecolour", colour) 105 } 106 problems := validate(t, buildICO(frame{width: 0, height: 0, data: opaque})) 107 p, ok := find(problems, "no alpha channel") 108 if !ok { 109 t.Fatalf("no problem reported, got %v", problems) 110 } 111 if p.Severity != Invisible { 112 t.Errorf("severity is %s, want %s", p.Severity, Invisible) 113 } 114 if !strings.Contains(p.Icon, "256x256") { 115 t.Errorf("problem names %q, want the 256 pixel icon", p.Icon) 116 } 117 } 118 119 func TestValidateAcceptsAPNGWithAlpha(t *testing.T) { 120 withA := encodePNG(t, withAlpha{solid(256, color.NRGBA{R: 1, G: 2, B: 3, A: 255})}) 121 if _, _, colour, ok := ihdr(withA); !ok || colour != 6 { 122 t.Fatalf("fixture is colour type %d, want truecolour with alpha", colour) 123 } 124 problems := validate(t, buildICO(frame{width: 0, height: 0, data: withA})) 125 if _, ok := find(problems, "alpha channel"); ok { 126 t.Errorf("reported a problem for a frame that has one: %v", problems) 127 } 128 } 129 130 func TestValidateReportsSizeDisagreement(t *testing.T) { 131 // The directory says 64, the image is 32. 132 small := encodePNG(t, withAlpha{gradient(32)}) 133 problems := validate(t, buildICO(frame{width: 64, height: 64, data: small})) 134 p, ok := find(problems, "the directory lists it as 64x64") 135 if !ok { 136 t.Fatalf("no problem reported, got %v", problems) 137 } 138 if p.Severity != Degraded { 139 t.Errorf("severity is %s, want %s", p.Severity, Degraded) 140 } 141 } 142 143 func TestValidateReportsDuplicateSizes(t *testing.T) { 144 one := encodePNG(t, withAlpha{gradient(32)}) 145 two := encodePNG(t, withAlpha{solid(32, color.NRGBA{A: 255})}) 146 problems := validate(t, buildICO( 147 frame{width: 32, height: 32, data: one}, 148 frame{width: 32, height: 32, data: two}, 149 )) 150 p, ok := find(problems, "2 icons of 32x32") 151 if !ok { 152 t.Fatalf("no problem reported, got %v", problems) 153 } 154 if p.Severity != Degraded { 155 t.Errorf("severity is %s, want %s", p.Severity, Degraded) 156 } 157 } 158 159 func TestValidateReportsMissingSizes(t *testing.T) { 160 var buf bytes.Buffer 161 if err := Encode(&buf, gradient(16)); err != nil { 162 t.Fatalf("encoding: %v", err) 163 } 164 problems := validate(t, buf.Bytes()) 165 p, ok := find(problems, "holds no") 166 if !ok { 167 t.Fatalf("no problem reported, got %v", problems) 168 } 169 if p.Severity != Advice { 170 t.Errorf("severity is %s, want %s", p.Severity, Advice) 171 } 172 for _, want := range []string{"32x32", "48x48", "256x256"} { 173 if !strings.Contains(p.Message, want) { 174 t.Errorf("message does not name %s: %s", want, p.Message) 175 } 176 } 177 } 178 179 func TestValidateReportsATruncatedBitmap(t *testing.T) { 180 problems := validate(t, buildICO(frame{width: 32, height: 32, data: []byte{1, 2, 3}})) 181 p, ok := find(problems, "too few for a bitmap header") 182 if !ok { 183 t.Fatalf("no problem reported, got %v", problems) 184 } 185 if p.Severity != Invisible { 186 t.Errorf("severity is %s, want %s", p.Severity, Invisible) 187 } 188 } 189 190 func TestValidateOrdersBySeverity(t *testing.T) { 191 // One frame Windows passes over, at a size that leaves others missing. 192 opaque := encodePNG(t, solid(256, color.NRGBA{R: 1, A: 255})) 193 problems := validate(t, buildICO(frame{width: 0, height: 0, data: opaque})) 194 if len(problems) < 2 { 195 t.Fatalf("want several problems, got %v", problems) 196 } 197 for i := 1; i < len(problems); i++ { 198 if problems[i-1].Severity > problems[i].Severity { 199 t.Fatalf("problem %d is less serious than the one after it: %v", i-1, problems) 200 } 201 } 202 if problems[0].Severity != Invisible { 203 t.Errorf("first problem is %s, want %s", problems[0].Severity, Invisible) 204 } 205 } 206 207 func TestValidateRejectsWhatItCannotParse(t *testing.T) { 208 for _, data := range [][]byte{nil, []byte("not an icon"), {0, 0, 1, 0, 0, 0}} { 209 if _, err := Validate(bytes.NewReader(data)); err == nil { 210 t.Errorf("validating %q returned no error", data) 211 } 212 } 213 }