check_test.go (6033B)
1 package main 2 3 import ( 4 "bytes" 5 "encoding/binary" 6 "image" 7 "image/color" 8 "image/png" 9 "os" 10 "strings" 11 "testing" 12 13 "github.com/jackmordaunt/icns/v4" 14 "github.com/jackmordaunt/icns/v4/ico" 15 ) 16 17 func art(side int) image.Image { 18 img := image.NewNRGBA(image.Rect(0, 0, side, side)) 19 for y := 0; y < side; y++ { 20 for x := 0; x < side; x++ { 21 img.SetNRGBA(x, y, color.NRGBA{ 22 R: uint8(x * 255 / side), 23 G: uint8(y * 255 / side), 24 B: 0x40, 25 A: uint8(255 - y*128/side), 26 }) 27 } 28 } 29 return img 30 } 31 32 func encoded(t *testing.T, write func(*bytes.Buffer) error) []byte { 33 t.Helper() 34 var buf bytes.Buffer 35 if err := write(&buf); err != nil { 36 t.Fatalf("encoding: %v", err) 37 } 38 return buf.Bytes() 39 } 40 41 // opaqueICO holds one PNG frame with no alpha channel, which Windows passes 42 // over. 43 func opaqueICO(t *testing.T) []byte { 44 t.Helper() 45 solid := image.NewNRGBA(image.Rect(0, 0, 256, 256)) 46 for i := 0; i < len(solid.Pix); i += 4 { 47 solid.Pix[i], solid.Pix[i+1], solid.Pix[i+2], solid.Pix[i+3] = 9, 9, 9, 255 48 } 49 frame := encoded(t, func(b *bytes.Buffer) error { return png.Encode(b, solid) }) 50 out := make([]byte, 0, 22+len(frame)) 51 out = binary.LittleEndian.AppendUint16(out, 0) 52 out = binary.LittleEndian.AppendUint16(out, 1) 53 out = binary.LittleEndian.AppendUint16(out, 1) 54 out = append(out, 0, 0, 0, 0) 55 out = binary.LittleEndian.AppendUint16(out, 1) 56 out = binary.LittleEndian.AppendUint16(out, 32) 57 out = binary.LittleEndian.AppendUint32(out, uint32(len(frame))) 58 out = binary.LittleEndian.AppendUint32(out, 22) 59 return append(out, frame...) 60 } 61 62 // bundleICNS holds only the small PNG type that an app bundle will not 63 // render. 64 func bundleICNS(t *testing.T) []byte { 65 t.Helper() 66 frame := encoded(t, func(b *bytes.Buffer) error { return png.Encode(b, art(16)) }) 67 element := func(id string, payload []byte) []byte { 68 out := append([]byte{}, id...) 69 out = binary.BigEndian.AppendUint32(out, uint32(8+len(payload))) 70 return append(out, payload...) 71 } 72 return element("icns", element("icp4", frame)) 73 } 74 75 func TestCheckPassesOurOwnOutput(t *testing.T) { 76 for _, tt := range []struct { 77 name string 78 data []byte 79 }{ 80 {"icns", encoded(t, func(b *bytes.Buffer) error { return icns.Encode(b, art(1024)) })}, 81 {"ico", encoded(t, func(b *bytes.Buffer) error { return ico.Encode(b, art(256)) })}, 82 } { 83 t.Run(tt.name, func(t *testing.T) { 84 if err := check("icon."+tt.name, bytes.NewReader(tt.data)); err != nil { 85 t.Errorf("check reported %v", err) 86 } 87 }) 88 } 89 } 90 91 func TestCheckFailsOnFindingsThatShow(t *testing.T) { 92 for _, tt := range []struct { 93 name string 94 data []byte 95 }{ 96 {"icon.ico", opaqueICO(t)}, 97 {"icon.icns", bundleICNS(t)}, 98 } { 99 t.Run(tt.name, func(t *testing.T) { 100 err := check(tt.name, bytes.NewReader(tt.data)) 101 if err == nil { 102 t.Fatal("check reported nothing") 103 } 104 if !strings.Contains(err.Error(), "change what is drawn") { 105 t.Errorf("error is %v, want it to count the findings", err) 106 } 107 if !strings.Contains(err.Error(), tt.name) { 108 t.Errorf("error does not name the file: %v", err) 109 } 110 }) 111 } 112 } 113 114 func TestCheckRejectsWhatIsNotAnIcon(t *testing.T) { 115 plain := encoded(t, func(b *bytes.Buffer) error { return png.Encode(b, art(64)) }) 116 err := check("art.png", bytes.NewReader(plain)) 117 if err == nil || !strings.Contains(err.Error(), "not an icns, ico or Windows binary") { 118 t.Errorf("check returned %v, want it to refuse a plain image", err) 119 } 120 } 121 122 // TestContainerReadsTheBytesFirst keeps a misnamed file from being validated 123 // against the wrong format. 124 func TestContainerReadsTheBytesFirst(t *testing.T) { 125 for _, tt := range []struct { 126 name string 127 data []byte 128 ext string 129 want string 130 }{ 131 {"icns named ico", encoded(t, func(b *bytes.Buffer) error { return icns.Encode(b, art(64)) }), ".ico", ".icns"}, 132 {"ico named icns", encoded(t, func(b *bytes.Buffer) error { return ico.Encode(b, art(64)) }), ".icns", ".ico"}, 133 {"unknown bytes, known extension", []byte("rubbish"), ".icns", ".icns"}, 134 {"unknown bytes, plain extension", []byte("rubbish"), ".png", ""}, 135 {"a binary named exe", []byte("MZ\x90\x00"), ".exe", ".exe"}, 136 {"a binary named dll", []byte("MZ\x90\x00"), ".dll", ".dll"}, 137 {"a binary named nothing", []byte("MZ\x90\x00"), "", ".exe"}, 138 } { 139 t.Run(tt.name, func(t *testing.T) { 140 if got := container(tt.data, tt.ext); got != tt.want { 141 t.Errorf("container = %q, want %q", got, tt.want) 142 } 143 }) 144 } 145 } 146 147 // TestCheckReadsABinary uses the exe package's fixture, a DLL whose resource 148 // section mingw laid out, rather than building another one here. 149 func TestCheckReadsABinary(t *testing.T) { 150 const fixture = "../../exe/testdata/icon.dll" 151 data, err := os.ReadFile(fixture) 152 if err != nil { 153 t.Fatalf("reading fixture: %v", err) 154 } 155 // The fixture holds 32, 24 and 16, so the only finding is the advice 156 // that larger sizes are absent. 157 if err := check("icon.dll", bytes.NewReader(data)); err != nil { 158 t.Errorf("check reported %v", err) 159 } 160 } 161 162 func TestCheckRefusesABinaryWithoutIcons(t *testing.T) { 163 data, err := os.ReadFile("../../exe/testdata/plain.dll") 164 if err != nil { 165 t.Fatalf("reading fixture: %v", err) 166 } 167 err = check("plain.dll", bytes.NewReader(data)) 168 if err == nil || !strings.Contains(err.Error(), "no icons found") { 169 t.Errorf("check returned %v, want it to report no icons", err) 170 } 171 } 172 173 // TestWritableCoversEveryFormatTheUsageOffers keeps the usage text and what 174 // the program will actually write from drifting apart. 175 func TestWritableCoversEveryFormatTheUsageOffers(t *testing.T) { 176 for _, ext := range []string{".icns", ".icon", ".ico", ".png", ".jpg", ".jpeg"} { 177 if !writable(ext) { 178 t.Errorf("writable(%q) is false", ext) 179 } 180 } 181 for _, ext := range []string{".exe", ".dll", ".gif", ""} { 182 if writable(ext) { 183 t.Errorf("writable(%q) is true", ext) 184 } 185 } 186 } 187 188 func TestNameLabelsAPipe(t *testing.T) { 189 if got := name(""); got != "stdin" { 190 t.Errorf("name(\"\") = %q, want stdin", got) 191 } 192 if got := name("/tmp/art/icon.icns"); got != "icon.icns" { 193 t.Errorf("name = %q, want the base name", got) 194 } 195 }