validate_test.go (8762B)
1 package icns 2 3 import ( 4 "bytes" 5 "image" 6 "image/png" 7 "strings" 8 "testing" 9 ) 10 11 func validate(t *testing.T, data []byte) []Problem { 12 t.Helper() 13 problems, err := Validate(bytes.NewReader(data)) 14 if err != nil { 15 t.Fatalf("validating: %v", err) 16 } 17 return problems 18 } 19 20 // found returns the first problem whose message contains want. 21 func found(problems []Problem, want string) (Problem, bool) { 22 for _, p := range problems { 23 if strings.Contains(p.Message, want) { 24 return p, true 25 } 26 } 27 return Problem{}, false 28 } 29 30 // flat returns an image of one colour, which the run-length encoder 31 // compresses rather than storing at its exact length. 32 func flat(side int) image.Image { 33 img := image.NewNRGBA(image.Rect(0, 0, side, side)) 34 for i := 0; i < len(img.Pix); i += 4 { 35 img.Pix[i], img.Pix[i+1], img.Pix[i+2], img.Pix[i+3] = 0x20, 0x40, 0x60, 0xFF 36 } 37 return img 38 } 39 40 // pngOf encodes an image of a given side, so an element can be given a 41 // payload of a size its type does not name. 42 func pngOf(t testing.TB, side int) []byte { 43 t.Helper() 44 var buf bytes.Buffer 45 if err := png.Encode(&buf, flat(side)); err != nil { 46 t.Fatal(err) 47 } 48 return buf.Bytes() 49 } 50 51 func TestValidateAcceptsOurOwnOutput(t *testing.T) { 52 var buf bytes.Buffer 53 if err := Encode(&buf, gradient(1024)); err != nil { 54 t.Fatalf("encoding: %v", err) 55 } 56 if problems := validate(t, buf.Bytes()); len(problems) != 0 { 57 for _, p := range problems { 58 t.Errorf("unexpected problem: %s", p) 59 } 60 } 61 } 62 63 // TestValidateReportsUnpaddedPlanes covers the reason padRLE exists: without 64 // the trailing byte, Apple silicon drops the last run. 65 func TestValidateReportsUnpaddedPlanes(t *testing.T) { 66 planes, mask := splitPlanes(flat(32), 32) 67 packed := packRLE(planes) 68 if len(packed) == len(planes) { 69 t.Fatal("fixture did not compress, so there is no run to drop") 70 } 71 data := file( 72 encodeElement("il32", packed), 73 encodeElement("l8mk", mask), 74 ) 75 p, ok := found(validate(t, data), "Apple silicon drops") 76 if !ok { 77 t.Fatalf("no problem reported, got %v", validate(t, data)) 78 } 79 if p.Severity != Degraded { 80 t.Errorf("severity is %s, want %s", p.Severity, Degraded) 81 } 82 } 83 84 func TestValidateAcceptsPaddedPlanes(t *testing.T) { 85 planes, mask := splitPlanes(flat(32), 32) 86 data := file( 87 encodeElement("il32", padRLE(packRLE(planes), len(planes))), 88 encodeElement("l8mk", mask), 89 ) 90 if _, ok := found(validate(t, data), "Apple silicon drops"); ok { 91 t.Errorf("reported padding that is present: %v", validate(t, data)) 92 } 93 } 94 95 // TestValidateAcceptsUncompressedPlanes covers the other half of the rule: 96 // planes stored at their exact length hold no run to drop. 97 func TestValidateAcceptsUncompressedPlanes(t *testing.T) { 98 // No three bytes in a row are equal, so every run is a literal and the 99 // encoder stores the planes as they are. 100 planes := make([]byte, 32*32*3) 101 for i := range planes { 102 planes[i] = byte(i % 251) 103 } 104 mask := make([]byte, 32*32) 105 packed := padRLE(packRLE(planes), len(planes)) 106 if len(packed) != len(planes) { 107 t.Fatalf("fixture compressed to %d of %d bytes, so it is not stored", len(packed), len(planes)) 108 } 109 data := file( 110 encodeElement("il32", packed), 111 encodeElement("l8mk", mask), 112 ) 113 if _, ok := found(validate(t, data), "Apple silicon drops"); ok { 114 t.Errorf("reported padding for uncompressed planes: %v", validate(t, data)) 115 } 116 } 117 118 // TestValidateLeavesARGBAlone records why the padding check covers only the 119 // three plane types. actool on macOS 26 compiled an icon into an icns whose 120 // ic04 is ARGB with its stream ending exactly on the last run, so warning 121 // about that shape would be accusing Apple's own output. 122 func TestValidateLeavesARGBAlone(t *testing.T) { 123 const side = 16 124 planes := make([]byte, side*side*4) 125 for i := range planes { 126 planes[i] = byte(0x40 + i/(side*side)) 127 } 128 packed := packRLE(planes) 129 if len(packed) == len(planes) { 130 t.Fatal("fixture did not compress, so there is no run to end on") 131 } 132 data := file(encodeElement("ic04", append([]byte("ARGB"), packed...))) 133 problems := validate(t, data) 134 if _, ok := found(problems, "Apple silicon drops"); ok { 135 t.Errorf("reported padding for an ARGB icon: %v", problems) 136 } 137 } 138 139 func TestValidateReportsAMissingMask(t *testing.T) { 140 planes, _ := splitPlanes(flat(32), 32) 141 data := file(encodeElement("il32", padRLE(packRLE(planes), len(planes)))) 142 p, ok := found(validate(t, data), "draws fully opaque") 143 if !ok { 144 t.Fatalf("no problem reported, got %v", validate(t, data)) 145 } 146 if p.Severity != Degraded { 147 t.Errorf("severity is %s, want %s", p.Severity, Degraded) 148 } 149 if !strings.Contains(p.Message, "l8mk") { 150 t.Errorf("message does not name the mask element: %s", p.Message) 151 } 152 } 153 154 func TestValidateReportsIcp4WithoutIs32(t *testing.T) { 155 data := file(encodeElement("icp4", pngOf(t, 16))) 156 p, ok := found(validate(t, data), "does not render from an app bundle") 157 if !ok { 158 t.Fatalf("no problem reported, got %v", validate(t, data)) 159 } 160 if p.Severity != Degraded { 161 t.Errorf("severity is %s, want %s", p.Severity, Degraded) 162 } 163 if !strings.Contains(p.Icon, "icp4") { 164 t.Errorf("problem names %q, want icp4", p.Icon) 165 } 166 } 167 168 func TestValidateAcceptsIcp4BesideIs32(t *testing.T) { 169 planes, mask := splitPlanes(flat(16), 16) 170 data := file( 171 encodeElement("icp4", pngOf(t, 16)), 172 encodeElement("is32", padRLE(packRLE(planes), len(planes))), 173 encodeElement("s8mk", mask), 174 ) 175 if _, ok := found(validate(t, data), "does not render from an app bundle"); ok { 176 t.Errorf("reported a bundle problem for a file that holds is32: %v", validate(t, data)) 177 } 178 } 179 180 func TestValidateReportsSizeDisagreement(t *testing.T) { 181 // ic07 names 128 pixels; the payload holds 64. 182 data := file(encodeElement("ic07", pngOf(t, 64))) 183 p, ok := found(validate(t, data), "where the type is 128x128") 184 if !ok { 185 t.Fatalf("no problem reported, got %v", validate(t, data)) 186 } 187 if p.Severity != Degraded { 188 t.Errorf("severity is %s, want %s", p.Severity, Degraded) 189 } 190 } 191 192 func TestValidateReportsJPEG2000AsAdvice(t *testing.T) { 193 data := file( 194 encodeElement("ic10", jpeg2000header), 195 encodeElement("ic07", pngOf(t, 128)), 196 ) 197 p, ok := found(validate(t, data), "JPEG 2000") 198 if !ok { 199 t.Fatalf("no problem reported, got %v", validate(t, data)) 200 } 201 if p.Severity != Advice { 202 t.Errorf("severity is %s, want %s", p.Severity, Advice) 203 } 204 } 205 206 func TestValidateReportsGapsBelowTheLargest(t *testing.T) { 207 data := file( 208 encodeElement("ic09", pngOf(t, 512)), 209 encodeElement("ic07", pngOf(t, 128)), 210 ) 211 p, ok := found(validate(t, data), "holds no") 212 if !ok { 213 t.Fatalf("no problem reported, got %v", validate(t, data)) 214 } 215 if p.Severity != Advice { 216 t.Errorf("severity is %s, want %s", p.Severity, Advice) 217 } 218 for _, want := range []string{"ic13", "ic08", "ic12", "ic11", "il32", "is32"} { 219 if !strings.Contains(p.Message, want) { 220 t.Errorf("message does not name %s: %s", want, p.Message) 221 } 222 } 223 // ic10 is 1024, above the largest icon present, so the artwork simply 224 // ran out and it is not reported. 225 if strings.Contains(p.Message, "ic10") { 226 t.Errorf("message names a size above the largest held: %s", p.Message) 227 } 228 } 229 230 func TestValidateReportsAnUndecodableIcon(t *testing.T) { 231 // An element whose type stores colour planes, holding too little to 232 // expand. 233 data := file( 234 encodeElement("il32", []byte{0xFF, 0x01}), 235 encodeElement("l8mk", make([]byte, 32*32)), 236 ) 237 p, ok := found(validate(t, data), "cannot be decoded") 238 if !ok { 239 t.Fatalf("no problem reported, got %v", validate(t, data)) 240 } 241 if p.Severity != Invisible { 242 t.Errorf("severity is %s, want %s", p.Severity, Invisible) 243 } 244 } 245 246 func TestValidateOrdersBySeverity(t *testing.T) { 247 planes, _ := splitPlanes(flat(32), 32) 248 data := file( 249 encodeElement("il32", padRLE(packRLE(planes), len(planes))), 250 encodeElement("ic10", jpeg2000header), 251 ) 252 problems := validate(t, data) 253 if len(problems) < 2 { 254 t.Fatalf("want several problems, got %v", problems) 255 } 256 for i := 1; i < len(problems); i++ { 257 if problems[i-1].Severity > problems[i].Severity { 258 t.Fatalf("problem %d is less serious than the one after it: %v", i-1, problems) 259 } 260 } 261 } 262 263 func TestValidateRejectsWhatItCannotParse(t *testing.T) { 264 for _, data := range [][]byte{nil, []byte("not an icns"), file()} { 265 if _, err := Validate(bytes.NewReader(data)); err == nil { 266 t.Errorf("validating %q returned no error", data) 267 } 268 } 269 } 270 271 // TestValidateIgnoresColourOfMask keeps the mask elements from being read as 272 // icons in their own right. 273 func TestValidateIgnoresColourOfMask(t *testing.T) { 274 planes, mask := splitPlanes(flat(16), 16) 275 data := file( 276 encodeElement("is32", padRLE(packRLE(planes), len(planes))), 277 encodeElement("s8mk", mask), 278 ) 279 for _, p := range validate(t, data) { 280 if strings.Contains(p.Icon, "s8mk") { 281 t.Errorf("reported the mask as an icon: %s", p) 282 } 283 } 284 }