bmp_test.go (6333B)
1 package ico 2 3 import ( 4 "bytes" 5 "encoding/binary" 6 "errors" 7 "image/color" 8 "testing" 9 ) 10 11 // colourTable builds a table of n entries where index 1 is c. 12 func colourTable(n int, c color.NRGBA) []byte { 13 out := make([]byte, n*4) 14 out[4], out[5], out[6] = c.B, c.G, c.R 15 return out 16 } 17 18 // bmpIcon builds one bitmap icon of the given depth, every pixel the same 19 // colour, with an all opaque mask. 20 func bmpIcon(size, bits int, c color.NRGBA) []byte { 21 var ( 22 stride = ((size*bits + 31) / 32) * 4 23 maskStride = ((size + 31) / 32) * 4 24 palette []byte 25 fill byte 26 ) 27 switch bits { 28 case 1: 29 palette, fill = colourTable(2, c), 0xFF 30 case 4: 31 palette, fill = colourTable(16, c), 0x11 32 case 8: 33 palette, fill = colourTable(256, c), 0x01 34 } 35 out := make([]byte, 0, headerSize+len(palette)+stride*size+maskStride*size) 36 out = binary.LittleEndian.AppendUint32(out, headerSize) 37 out = binary.LittleEndian.AppendUint32(out, uint32(size)) 38 out = binary.LittleEndian.AppendUint32(out, uint32(size*2)) 39 out = binary.LittleEndian.AppendUint16(out, 1) 40 out = binary.LittleEndian.AppendUint16(out, uint16(bits)) 41 out = binary.LittleEndian.AppendUint32(out, 0) 42 out = binary.LittleEndian.AppendUint32(out, 0) 43 out = binary.LittleEndian.AppendUint32(out, 0) 44 out = binary.LittleEndian.AppendUint32(out, 0) 45 out = binary.LittleEndian.AppendUint32(out, uint32(len(palette)/4)) 46 out = binary.LittleEndian.AppendUint32(out, 0) 47 out = append(out, palette...) 48 49 rows := make([]byte, stride*size) 50 switch bits { 51 case 32: 52 for y := 0; y < size; y++ { 53 row := rows[y*stride:] 54 for x := 0; x < size; x++ { 55 row[x*4], row[x*4+1], row[x*4+2], row[x*4+3] = c.B, c.G, c.R, c.A 56 } 57 } 58 case 24: 59 for y := 0; y < size; y++ { 60 row := rows[y*stride:] 61 for x := 0; x < size; x++ { 62 row[x*3], row[x*3+1], row[x*3+2] = c.B, c.G, c.R 63 } 64 } 65 default: 66 for i := range rows { 67 rows[i] = fill 68 } 69 } 70 out = append(out, rows...) 71 return append(out, make([]byte, maskStride*size)...) 72 } 73 74 // icoFile wraps one icon in a directory. 75 func icoFile(size int, data []byte) []byte { 76 out := make([]byte, 0, directorySize+entrySize+len(data)) 77 out = binary.LittleEndian.AppendUint16(out, 0) 78 out = binary.LittleEndian.AppendUint16(out, 1) 79 out = binary.LittleEndian.AppendUint16(out, 1) 80 out = append(out, byte(size), byte(size), 0, 0) 81 out = binary.LittleEndian.AppendUint16(out, 1) 82 out = binary.LittleEndian.AppendUint16(out, 32) 83 out = binary.LittleEndian.AppendUint32(out, uint32(len(data))) 84 out = binary.LittleEndian.AppendUint32(out, uint32(directorySize+entrySize)) 85 return append(out, data...) 86 } 87 88 // TestDecodeDepths covers the bit depths older icons are written at, which 89 // carry their own colour table and take their alpha from the mask. 90 func TestDecodeDepths(t *testing.T) { 91 t.Parallel() 92 for _, bits := range []int{1, 4, 8, 24, 32} { 93 t.Run(map[int]string{1: "one bit", 4: "four bit", 8: "eight bit", 24: "twenty four bit", 32: "thirty two bit"}[bits], func(st *testing.T) { 94 want := color.NRGBA{R: 0x30, G: 0x90, B: 0xC0, A: 0xFF} 95 img, err := Decode(bytes.NewReader(icoFile(32, bmpIcon(32, bits, want)))) 96 if err != nil { 97 st.Fatal(err) 98 } 99 if got := img.Bounds().Dx(); got != 32 { 100 st.Fatalf("decoded a %dpx icon, want 32", got) 101 } 102 if got := centre(img); got != want { 103 st.Fatalf("centre = %v, want %v", got, want) 104 } 105 }) 106 } 107 } 108 109 // TestDecodeMaskWhenAlphaIsEmpty covers the writers that left the alpha 110 // channel of a 32 bit icon at zero and meant the mask to be read. 111 func TestDecodeMaskWhenAlphaIsEmpty(t *testing.T) { 112 t.Parallel() 113 const size = 16 114 data := bmpIcon(size, 32, color.NRGBA{R: 0x80, G: 0x40, B: 0x20, A: 0x00}) 115 img, err := Decode(bytes.NewReader(icoFile(size, data))) 116 if err != nil { 117 t.Fatal(err) 118 } 119 // The mask is all opaque, so the icon has to come back opaque rather 120 // than invisible. 121 if got := centre(img); got.A != 0xFF { 122 t.Fatalf("centre = %v, want it opaque from the mask", got) 123 } 124 } 125 126 func TestDecodeMalformed(t *testing.T) { 127 t.Parallel() 128 valid := bmpIcon(16, 32, color.NRGBA{A: 0xFF}) 129 tests := []struct { 130 desc string 131 data []byte 132 want error 133 }{ 134 {"empty", nil, ErrInvalidHeader}, 135 {"short", []byte{0, 0}, ErrInvalidHeader}, 136 {"reserved is not zero", []byte{1, 0, 1, 0, 1, 0}, ErrInvalidHeader}, 137 {"a cursor, not an icon", []byte{0, 0, 2, 0, 1, 0}, ErrInvalidHeader}, 138 {"no entries", []byte{0, 0, 1, 0, 0, 0}, ErrNoIcons}, 139 {"truncated directory", []byte{0, 0, 1, 0, 2, 0, 1, 2, 3}, ErrMalformed}, 140 {"icon lies outside the file", icoFile(16, valid)[:directorySize+entrySize+4], ErrMalformed}, 141 {"bitmap header is short", icoFile(16, []byte{1, 2, 3}), ErrMalformed}, 142 {"pixels do not fit", icoFile(16, bmpIcon(16, 32, color.NRGBA{})[:headerSize+16]), ErrMalformed}, 143 } 144 for _, tt := range tests { 145 t.Run(tt.desc, func(st *testing.T) { 146 if _, err := Decode(bytes.NewReader(tt.data)); !errors.Is(err, tt.want) { 147 st.Fatalf("error = %v, want %v", err, tt.want) 148 } 149 }) 150 } 151 } 152 153 func TestDecodeUnsupported(t *testing.T) { 154 t.Parallel() 155 // Sixteen bits per pixel, which this package does not read. 156 odd := bmpIcon(16, 32, color.NRGBA{A: 0xFF}) 157 binary.LittleEndian.PutUint16(odd[14:16], 16) 158 if _, err := Decode(bytes.NewReader(icoFile(16, odd))); !errors.Is(err, ErrUnsupportedFormat) { 159 t.Errorf("error = %v, want ErrUnsupportedFormat", err) 160 } 161 // A compressed bitmap, which this package does not read either. 162 compressed := bmpIcon(16, 32, color.NRGBA{A: 0xFF}) 163 binary.LittleEndian.PutUint32(compressed[16:20], 1) 164 if _, err := Decode(bytes.NewReader(icoFile(16, compressed))); !errors.Is(err, ErrUnsupportedFormat) { 165 t.Errorf("error = %v, want ErrUnsupportedFormat", err) 166 } 167 } 168 169 // FuzzDecode checks that arbitrary input never panics or hangs the decoder. 170 func FuzzDecode(f *testing.F) { 171 var valid bytes.Buffer 172 if err := Encode(&valid, gradient(64)); err != nil { 173 f.Fatal(err) 174 } 175 f.Add(valid.Bytes()) 176 f.Add([]byte{}) 177 f.Add([]byte{0, 0, 1, 0, 0, 0}) 178 for _, bits := range []int{1, 4, 8, 24, 32} { 179 f.Add(icoFile(16, bmpIcon(16, bits, color.NRGBA{A: 0xFF}))) 180 } 181 f.Add(icoFile(16, []byte{1, 2, 3})) 182 f.Fuzz(func(t *testing.T, data []byte) { 183 if d, err := NewDecoder(bytes.NewReader(data)); err == nil { 184 for _, icon := range d.Icons() { 185 icon.Decode() 186 } 187 } 188 Decode(bytes.NewReader(data)) 189 DecodeAll(bytes.NewReader(data)) 190 }) 191 }