// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. package notify import ( "bytes" "image" "image/color" "image/png" "testing" "github.com/godbus/dbus/v5" ) // rawVariant builds the (iiibiiay) struct the bus delivers for image-data. func rawVariant(w, h, stride int, alpha bool, ch int, data []byte) dbus.Variant { return dbus.MakeVariant([]interface{}{ int32(w), int32(h), int32(stride), alpha, int32(8), int32(ch), data, }) } func TestImageDataFromHints(t *testing.T) { // 2x1 RGBA: red, green. rgba := []byte{255, 0, 0, 255, 0, 255, 0, 255} cases := []struct { name string hints map[string]dbus.Variant wantW int want bool }{ {"image-data wins", map[string]dbus.Variant{ "image-data": rawVariant(2, 1, 8, true, 4, rgba), "icon_data": rawVariant(9, 9, 8, true, 4, rgba), "image-path": dbus.MakeVariant("/tmp/x.png"), }, 2, true}, {"image_data alias", map[string]dbus.Variant{ "image_data": rawVariant(2, 1, 8, true, 4, rgba), }, 2, true}, {"icon_data is not tier 1", map[string]dbus.Variant{ "icon_data": rawVariant(2, 1, 8, true, 4, rgba), }, 0, false}, {"absent", map[string]dbus.Variant{}, 0, false}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { got, ok := ImageDataFromHints(c.hints) if ok != c.want { t.Fatalf("ok = %v, want %v", ok, c.want) } if ok && got.Width != c.wantW { t.Fatalf("width = %d, want %d", got.Width, c.wantW) } }) } } func TestIconDataFromHints(t *testing.T) { rgba := []byte{255, 0, 0, 255, 0, 255, 0, 255} if got, ok := IconDataFromHints(map[string]dbus.Variant{ "icon_data": rawVariant(2, 1, 8, true, 4, rgba), }); !ok || got.Width != 2 { t.Fatalf("icon_data got %v ok=%v", got, ok) } if _, ok := IconDataFromHints(map[string]dbus.Variant{ "image-data": rawVariant(2, 1, 8, true, 4, rgba), }); ok { t.Fatal("image-data must not be read as icon_data") } } func TestImagePathFromHints(t *testing.T) { got, ok := ImagePathFromHints(map[string]dbus.Variant{"image-path": dbus.MakeVariant("/tmp/shot.png")}) if !ok || got != "/tmp/shot.png" { t.Fatalf("got %q ok=%v", got, ok) } if _, ok := ImagePathFromHints(map[string]dbus.Variant{}); ok { t.Fatal("empty hints must not report a path") } } // checkPixel asserts the RGBA of img at (x,y) matches want. func checkPixel(t *testing.T, img image.Image, x, y int, want color.RGBA) { t.Helper() r, g, b, a := img.At(x, y).RGBA() if r>>8 != uint32(want.R) || g>>8 != uint32(want.G) || b>>8 != uint32(want.B) || a>>8 != uint32(want.A) { t.Fatalf("pixel (%d,%d) = %d %d %d %d, want %v", x, y, r>>8, g>>8, b>>8, a>>8, want) } } func TestRawImagePNG(t *testing.T) { // 2x2 RGBA with a rowstride wider than Width*Channels. The second row is // the point: if stride were ignored, row 1 would read the row-0 padding. r := &RawImage{Width: 2, Height: 2, RowStride: 12, HasAlpha: true, BitsPerSample: 8, Channels: 4, Data: []byte{ 255, 0, 0, 255, 0, 255, 0, 255, 9, 9, 9, 9, // row 0: red, green, pad 0, 0, 255, 255, 255, 255, 255, 255, 9, 9, 9, 9, // row 1: blue, white, pad }} data, err := r.PNG() if err != nil { t.Fatalf("PNG: %v", err) } img, err := png.Decode(bytes.NewReader(data)) if err != nil { t.Fatalf("decode: %v", err) } if img.Bounds().Dx() != 2 || img.Bounds().Dy() != 2 { t.Fatalf("bounds = %v", img.Bounds()) } checkPixel(t, img, 0, 0, color.RGBA{255, 0, 0, 255}) checkPixel(t, img, 1, 0, color.RGBA{0, 255, 0, 255}) checkPixel(t, img, 0, 1, color.RGBA{0, 0, 255, 255}) checkPixel(t, img, 1, 1, color.RGBA{255, 255, 255, 255}) } func TestRawImagePNGThreeChannel(t *testing.T) { // 2x1 RGB, no alpha byte: PNG() must force opaque. r := &RawImage{Width: 2, Height: 1, RowStride: 6, HasAlpha: false, BitsPerSample: 8, Channels: 3, Data: []byte{255, 0, 0, 0, 255, 0}} data, err := r.PNG() if err != nil { t.Fatalf("PNG: %v", err) } img, err := png.Decode(bytes.NewReader(data)) if err != nil { t.Fatalf("decode: %v", err) } if img.Bounds().Dx() != 2 || img.Bounds().Dy() != 1 { t.Fatalf("bounds = %v", img.Bounds()) } checkPixel(t, img, 0, 0, color.RGBA{255, 0, 0, 255}) checkPixel(t, img, 1, 0, color.RGBA{0, 255, 0, 255}) } func TestRawImagePNGRejectsBadData(t *testing.T) { if _, err := (&RawImage{Width: 0, Height: 1, Channels: 4, BitsPerSample: 8}).PNG(); err == nil { t.Fatal("zero width must error") } if _, err := (&RawImage{Width: 1, Height: 1, Channels: 2, BitsPerSample: 8}).PNG(); err == nil { t.Fatal("channels 2 must error") } // 2x2 RGBA needs 16 bytes; the slice holds one row. if _, err := (&RawImage{Width: 2, Height: 2, RowStride: 8, BitsPerSample: 8, Channels: 4, Data: []byte{0, 0, 0, 255, 255, 255, 255, 255}}).PNG(); err == nil { t.Fatal("short data must error") } // Dimensions straight off the bus: with RowStride 0 the stride*(Height-1) // product overflows to negative and used to slip past the guard into // image.NewRGBA. Must error, not panic. if _, err := (&RawImage{ Width: 1<<31 - 1, Height: 1<<31 - 1, RowStride: 0, BitsPerSample: 8, Channels: 4, Data: []byte{0, 0, 0, 255}, }).PNG(); err == nil { t.Fatal("oversized dimensions must error") } }