icns

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

reader_test.go (8671B)


      1 package icns
      2 
      3 import (
      4 	"bytes"
      5 	"encoding/binary"
      6 	"errors"
      7 	"image"
      8 	"image/color"
      9 	"image/png"
     10 	"io"
     11 	"reflect"
     12 	"testing"
     13 )
     14 
     15 // encodeElement builds one icns element: 4-byte type, 4-byte big-endian
     16 // length of the whole element, then the payload.
     17 func encodeElement(id string, payload []byte) []byte {
     18 	out := make([]byte, 0, elementHeaderSize+len(payload))
     19 	out = append(out, id...)
     20 	out = binary.BigEndian.AppendUint32(out, uint32(elementHeaderSize+len(payload)))
     21 	return append(out, payload...)
     22 }
     23 
     24 // file wraps elements in an icns header with a correct length.
     25 func file(elements ...[]byte) []byte {
     26 	return encodeElement("icns", bytes.Join(elements, nil))
     27 }
     28 
     29 func pngBytes(t testing.TB, side int) []byte {
     30 	t.Helper()
     31 	var buf bytes.Buffer
     32 	if err := png.Encode(&buf, image.NewNRGBA(image.Rect(0, 0, side, side))); err != nil {
     33 		t.Fatal(err)
     34 	}
     35 	return buf.Bytes()
     36 }
     37 
     38 func TestDecodeMalformed(t *testing.T) {
     39 	t.Parallel()
     40 	tests := []struct {
     41 		desc string
     42 		data []byte
     43 		want error
     44 	}{
     45 		{"empty", nil, ErrInvalidHeader},
     46 		{"short", []byte("ic"), ErrInvalidHeader},
     47 		{"wrong magic", encodeElement("ICNS", nil), ErrInvalidHeader},
     48 		{"header only", file(), ErrNoIcons},
     49 		{"declared size exceeds data", append([]byte("icns"), 0xff, 0xff, 0xff, 0xff), ErrMalformed},
     50 		{"truncated element header", append([]byte("icns\x00\x00\x00\x0c"), 'i', 'c', '0', '7'), ErrMalformed},
     51 		{"element size below header", file(append([]byte("ic07"), 0, 0, 0, 4)), ErrMalformed},
     52 		{"element overruns file", file(append([]byte("ic07"), 0, 0, 1, 0)), ErrMalformed},
     53 		{"zero-length TOC loops forever without a check", file(append([]byte("TOC "), 0, 0, 0, 0)), ErrMalformed},
     54 		{"unknown elements only", file(encodeElement("TOC ", []byte{1, 2, 3, 4}), encodeElement("icnV", []byte{0, 0, 0, 0})), ErrNoIcons},
     55 		{"empty icon payload", file(encodeElement("ic07", nil)), ErrNoIcons},
     56 		{"only jpeg2000", file(encodeElement("ic07", jpeg2000header)), ErrUnsupportedFormat},
     57 	}
     58 	for _, tt := range tests {
     59 		t.Run(tt.desc, func(st *testing.T) {
     60 			_, err := Decode(bytes.NewReader(tt.data))
     61 			if !errors.Is(err, tt.want) {
     62 				st.Fatalf("Decode error = %v, want %v", err, tt.want)
     63 			}
     64 			if _, err := Probe(bytes.NewReader(tt.data)); err == nil && !errors.Is(tt.want, ErrUnsupportedFormat) {
     65 				st.Fatalf("Probe accepted data that Decode rejected with %v", tt.want)
     66 			}
     67 		})
     68 	}
     69 }
     70 
     71 func TestDecodeSkipsNonIconElements(t *testing.T) {
     72 	t.Parallel()
     73 	data := file(
     74 		encodeElement("TOC ", []byte("ic07\x00\x00\x00\x10")),
     75 		encodeElement("icnV", []byte{0x40, 0x00, 0x00, 0x00}),
     76 		encodeElement("name", []byte("icon")),
     77 		encodeElement("ic07", pngBytes(t, 128)),
     78 		encodeElement("ic11", pngBytes(t, 32)),
     79 	)
     80 	desc, err := Probe(bytes.NewReader(data))
     81 	if err != nil {
     82 		t.Fatal(err)
     83 	}
     84 	if len(desc) != 2 || desc[0].ID != "ic07" || desc[1].ID != "ic11" {
     85 		t.Fatalf("Probe = %v, want ic07 and ic11", desc)
     86 	}
     87 	// Trailing bytes beyond the declared file size are tolerated.
     88 	img, err := Decode(bytes.NewReader(append(data, "junk"...)))
     89 	if err != nil {
     90 		t.Fatal(err)
     91 	}
     92 	if got := img.Bounds().Dx(); got != 128 {
     93 		t.Fatalf("Decode returned a %dpx icon, want 128", got)
     94 	}
     95 }
     96 
     97 func TestDecodeFallsBackPastJPEG2000(t *testing.T) {
     98 	t.Parallel()
     99 	data := file(
    100 		encodeElement("ic10", jpeg2000header), // Largest, but undecodable.
    101 		encodeElement("ic07", pngBytes(t, 128)),
    102 	)
    103 	img, err := Decode(bytes.NewReader(data))
    104 	if err != nil {
    105 		t.Fatal(err)
    106 	}
    107 	if got := img.Bounds().Dx(); got != 128 {
    108 		t.Fatalf("Decode returned a %dpx icon, want the 128px PNG", got)
    109 	}
    110 	all, err := DecodeAll(bytes.NewReader(data))
    111 	if err != nil || len(all) != 1 {
    112 		t.Fatalf("DecodeAll = %d images, %v; want 1, nil", len(all), err)
    113 	}
    114 }
    115 
    116 func TestDecoder(t *testing.T) {
    117 	t.Parallel()
    118 	png128 := pngBytes(t, 128)
    119 	data := file(
    120 		encodeElement("ic11", pngBytes(t, 32)),
    121 		encodeElement("ic10", jpeg2000header),
    122 		encodeElement("ic07", png128),
    123 	)
    124 	d, err := NewDecoder(bytes.NewReader(data))
    125 	if err != nil {
    126 		t.Fatal(err)
    127 	}
    128 	icons := d.Icons()
    129 	var got []string
    130 	for _, icon := range icons {
    131 		got = append(got, icon.ID)
    132 	}
    133 	if want := []string{"ic10", "ic07", "ic11"}; !reflect.DeepEqual(got, want) {
    134 		t.Fatalf("icons = %v, want %v largest first", got, want)
    135 	}
    136 
    137 	// An icon this package cannot decode still hands over its bytes, so a
    138 	// caller can bring its own decoder.
    139 	if _, err := icons[0].Decode(); !errors.Is(err, ErrUnsupportedFormat) {
    140 		t.Errorf("decoding the JPEG 2000 icon = %v, want ErrUnsupportedFormat", err)
    141 	}
    142 	if !bytes.Equal(icons[0].Payload(), jpeg2000header) {
    143 		t.Error("the JPEG 2000 icon's payload is not the stored bytes")
    144 	}
    145 
    146 	img, err := icons[1].Decode()
    147 	if err != nil {
    148 		t.Fatal(err)
    149 	}
    150 	if got := img.Bounds().Dx(); got != 128 {
    151 		t.Errorf("decoded a %dpx icon, want 128", got)
    152 	}
    153 	if !bytes.Equal(icons[1].Payload(), png128) {
    154 		t.Error("the PNG icon's payload is not the stored file")
    155 	}
    156 
    157 	// The returned slice is the caller's to reorder.
    158 	icons[0] = Entry{}
    159 	if again := d.Icons(); again[0].ID != "ic10" {
    160 		t.Errorf("Icons was affected by a change to an earlier result: %v", again[0].ID)
    161 	}
    162 }
    163 
    164 // TestDecodeUsesRegisteredFormats checks that an element holding a whole
    165 // image file is handed to whatever decoder the program registered. That is
    166 // how a JPEG 2000 icon is read without this package depending on a codec for
    167 // it: the caller imports one, and these elements start decoding.
    168 func TestDecodeUsesRegisteredFormats(t *testing.T) {
    169 	// Registration is global and cannot be undone, so this uses a magic
    170 	// nothing else does and does not run in parallel.
    171 	const magic = "notarealformat"
    172 	want := color.NRGBA{R: 0x11, G: 0x22, B: 0x33, A: 0xFF}
    173 	image.RegisterFormat("notareal", magic,
    174 		func(r io.Reader) (image.Image, error) { return solid(64, want), nil },
    175 		func(r io.Reader) (image.Config, error) {
    176 			return image.Config{Width: 64, Height: 64, ColorModel: color.NRGBAModel}, nil
    177 		})
    178 
    179 	data := file(encodeElement("ic12", []byte(magic+" and then the pixels")))
    180 	img, err := Decode(bytes.NewReader(data))
    181 	if err != nil {
    182 		t.Fatalf("an element in a registered format did not decode: %v", err)
    183 	}
    184 	if got := centre(img); got != want {
    185 		t.Fatalf("decoded %v, want %v from the registered decoder", got, want)
    186 	}
    187 }
    188 
    189 // TestDecodeWithoutARegisteredFormat checks the other side of that: an icon
    190 // nothing can read is reported as unsupported rather than as corrupt, which
    191 // is what lets the callers above skip past it to a size they can read.
    192 func TestDecodeWithoutARegisteredFormat(t *testing.T) {
    193 	t.Parallel()
    194 	data := file(encodeElement("ic07", append(jpeg2000header, 1, 2, 3, 4)))
    195 	_, err := Decode(bytes.NewReader(data))
    196 	if !errors.Is(err, ErrUnsupportedFormat) {
    197 		t.Fatalf("error = %v, want ErrUnsupportedFormat", err)
    198 	}
    199 }
    200 
    201 // FuzzDecode checks that arbitrary input never panics or hangs the decoder.
    202 func FuzzDecode(f *testing.F) {
    203 	var valid bytes.Buffer
    204 	if err := Encode(&valid, gradient(64)); err != nil {
    205 		f.Fatal(err)
    206 	}
    207 	f.Add(valid.Bytes())
    208 	f.Add([]byte{})
    209 	f.Add(file())
    210 	f.Add(file(append([]byte("TOC "), 0, 0, 0, 0)))
    211 	f.Add(file(append([]byte("ic07"), 0, 0, 0, 4)))
    212 	f.Add(append([]byte("icns"), 0xff, 0xff, 0xff, 0xff))
    213 	// Legacy colour and mask elements, which run the RLE decoder.
    214 	rgb, mask, _ := legacyIcon(16)
    215 	f.Add(file(encodeElement("is32", rgb), encodeElement("s8mk", mask)))
    216 	f.Add(file(encodeElement("is32", rgb)))
    217 	f.Add(file(encodeElement("it32", []byte{0, 0, 0, 0, 0xFF, 0x01})))
    218 	f.Add(file(encodeElement("il32", []byte{0xFF}), encodeElement("l8mk", mask)))
    219 	// ARGB, which runs the same decoder over four planes.
    220 	argb, _ := argbElement(16)
    221 	f.Add(file(encodeElement("ic04", argb)))
    222 	f.Add(file(encodeElement("ic04", []byte("ARGB"))))
    223 	// Indexed icons, whose mask lives in a companion element.
    224 	f.Add(file(encodeElement("icl8", make([]byte, 32*32)), encodeElement("ICN#", bitmapMask(32, 32))))
    225 	f.Add(file(encodeElement("icl4", make([]byte, 32*32/2))))
    226 	f.Add(file(encodeElement("ICN#", bitmapMask(32, 32))))
    227 	f.Add(file(encodeElement("icm8", make([]byte, 16*12))))
    228 	f.Fuzz(func(t *testing.T, data []byte) {
    229 		Probe(bytes.NewReader(data))
    230 		Decode(bytes.NewReader(data))
    231 		DecodeAll(bytes.NewReader(data))
    232 	})
    233 }
    234 
    235 // gradient returns a side by side image whose colour and alpha vary with
    236 // position, so an encode/decode roundtrip cannot pass by accident.
    237 func gradient(side int) *image.NRGBA {
    238 	img := image.NewNRGBA(image.Rect(0, 0, side, side))
    239 	for y := 0; y < side; y++ {
    240 		for x := 0; x < side; x++ {
    241 			img.SetNRGBA(x, y, color.NRGBA{
    242 				R: uint8(x * 255 / side),
    243 				G: uint8(y * 255 / side),
    244 				B: uint8((x + y) * 255 / (2 * side)),
    245 				A: uint8(255 - y*255/(2*side)),
    246 			})
    247 		}
    248 	}
    249 	return img
    250 }