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 }