icns

Easily create .icns files (Mac Icons) with this Go library or the included CLI.
Log | Files | Refs | LICENSE

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 }