icns

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

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 }