diff --git a/.gitignore b/.gitignore index 5181c4c..33a420a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,7 @@ dist/ agent.yml !cmd/theta-agent-helper/ !cmd/theta-agent-helper/main.go +!cmd/theta-agent-tray/ +!cmd/theta-agent-tray/main_test.go # Fetched by scripts/setup-build-env.ps1, pinned in installer/windows/vendor-manifest.json installer/windows/vendor/ diff --git a/cmd/theta-agent-tray/main_test.go b/cmd/theta-agent-tray/main_test.go new file mode 100644 index 0000000..a579f48 --- /dev/null +++ b/cmd/theta-agent-tray/main_test.go @@ -0,0 +1,81 @@ +package main + +import ( + "bytes" + "runtime" + "testing" +) + +// pngToIco wraps a PNG in a Windows ICO with BMP (not PNG-compressed) entries, +// because LoadImage cannot read PNG-in-ICO. Verify the container is well-formed +// and every entry's DIB is a valid 32bpp bitmap with an AND mask. +func TestPNGToIco(t *testing.T) { + ico := pngToIco(iconGreen) + if len(ico) < 6+16 { + t.Fatal("ico too short") + } + + // ICONDIR + if ico[0] != 0 || ico[1] != 0 { + t.Errorf("reserved must be 0") + } + if ico[2] != 1 || ico[3] != 0 { + t.Errorf("type must be 1 (icon)") + } + count := int(ico[4]) | int(ico[5])<<8 + if count != 3 { + t.Fatalf("expected 3 icon entries, got %d", count) + } + + // Each ICONDIRENTRY: valid size, planes=1, bpp=32, DIB with BITMAPINFOHEADER. + for i := 0; i < count; i++ { + e := 6 + i*16 + w := int(ico[e]) + h := int(ico[e+1]) + if w == 0 { + w = 256 + } + if h == 0 { + h = 256 + } + planes := int(ico[e+4]) | int(ico[e+5])<<8 + bpp := int(ico[e+6]) | int(ico[e+7])<<8 + size := int(ico[e+8]) | int(ico[e+9])<<8 | int(ico[e+10])<<16 | int(ico[e+11])<<24 + offset := int(ico[e+12]) | int(ico[e+13])<<8 | int(ico[e+14])<<16 | int(ico[e+15])<<24 + + if planes != 1 || bpp != 32 { + t.Errorf("entry %d: want planes=1 bpp=32, got %d/%d", i, planes, bpp) + } + if offset+size > len(ico) { + t.Fatalf("entry %d: DIB out of range", i) + } + + dib := ico[offset : offset+size] + // BITMAPINFOHEADER: biSize=40, biHeight = 2x for XOR+AND. + biSize := int(dib[0]) | int(dib[1])<<8 | int(dib[2])<<16 | int(dib[3])<<24 + biW := int(dib[4]) | int(dib[5])<<8 | int(dib[6])<<16 | int(dib[7])<<24 + biH := int(dib[8]) | int(dib[9])<<8 | int(dib[10])<<16 | int(dib[11])<<24 + if biSize != 40 { + t.Errorf("entry %d: expected BITMAPINFOHEADER (40), got %d", i, biSize) + } + if biW != w || biH != h*2 { + t.Errorf("entry %d: DIB dims %dx%d, want %dx%d", i, biW, biH, w, h*2) + } + } +} + +// toWindowsIcon passes the PNG through untouched on non-Windows and returns an +// ICO on Windows (whose validity TestPNGToIco covers). +func TestToWindowsIcon(t *testing.T) { + pngMagic := []byte{0x89, 'P', 'N', 'G'} + if runtime.GOOS == "windows" { + if bytes.HasPrefix(toWindowsIcon(iconRed), pngMagic) { + t.Error("windows must not receive a raw PNG") + } + } else { + if !bytes.HasPrefix(toWindowsIcon(iconRed), pngMagic) { + t.Error("non-windows must receive the PNG unchanged") + } + } +} +