fix(cursor): check cursor buffer pitch and caps, validate Xcursor chunks
The cursor plane uses the smaller driver cap and refuses a padded dumb pitch, since the cursor ioctl assumes width*4. Xcursor chunks must match their table entry, Inherits is matched as a whole key, and the headless blend clamps non-premultiplied pixels. Adds a Decode fuzz target and an Inherits cycle test.
- commit
- 19bbdfa0fb1e1ee628c0af9bd443efccdf9be5e0
- author
- brice · · 2026-09-24 15:25 UTC
- parent
- c8e5e67
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
6 files · +62 -15
M internal/adapters/drm/cursor.go +24 -6
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package drm | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "fmt" | |
| 4 | 5 | "sync" | |
| 5 | 6 | "unsafe" | |
| 6 | 7 | ) | |
| @@ -43,17 +44,28 @@ type Cursor struct { | |||
| 43 | 44 | // newCursor allocates a cursor buffer at the driver's preferred size (64 when | |
| 44 | 45 | // the driver does not say). | |
| 45 | 46 | func newCursor(fd int, crtc uint32) (*Cursor, error) { | |
| 46 | - | size := 64 | |
| 47 | + | // The plane is square: the smaller of the width and height caps. | |
| 48 | + | size := 0 | |
| 47 | 49 | for _, c := range []uint64{capCursorW, capCursorH} { | |
| 48 | 50 | v := getCap{capability: c} | |
| 49 | - | if ioctl(fd, ioctlGetCap, unsafe.Pointer(&v)) == nil && v.value >= 32 && v.value <= 512 { | |
| 50 | - | size = max(size, int(v.value)) | |
| 51 | + | if ioctl(fd, ioctlGetCap, unsafe.Pointer(&v)) == nil && v.value >= 16 && v.value <= 512 { | |
| 52 | + | if size == 0 || int(v.value) < size { | |
| 53 | + | size = int(v.value) | |
| 54 | + | } | |
| 51 | 55 | } | |
| 52 | 56 | } | |
| 57 | + | if size == 0 { | |
| 58 | + | size = 64 | |
| 59 | + | } | |
| 53 | 60 | buf, err := newDumb(fd, size, size) | |
| 54 | 61 | if err != nil { | |
| 55 | 62 | return nil, err | |
| 56 | 63 | } | |
| 64 | + | // The cursor ioctl passes no pitch: drivers assume width*4. | |
| 65 | + | if int(buf.pitch) != size*4 { | |
| 66 | + | buf.destroy(fd) | |
| 67 | + | return nil, fmt.Errorf("cursor buffer pitch %d, want %d", buf.pitch, size*4) | |
| 68 | + | } | |
| 57 | 69 | return &Cursor{fd: fd, crtc: crtc, buf: buf, size: size}, nil | |
| 58 | 70 | } | |
| 59 | 71 | ||
| @@ -65,6 +77,9 @@ func (c *Cursor) Limit() int { return c.size } | |||
| 65 | 77 | func (c *Cursor) SetImage(pixels []byte, w, h, hotX, hotY int) error { | |
| 66 | 78 | c.mu.Lock() | |
| 67 | 79 | defer c.mu.Unlock() | |
| 80 | + | if c.buf == nil { | |
| 81 | + | return nil | |
| 82 | + | } | |
| 68 | 83 | pitch := int(c.buf.pitch) | |
| 69 | 84 | clear(c.buf.mem) | |
| 70 | 85 | for y := range min(h, c.size) { | |
| @@ -77,7 +92,7 @@ func (c *Cursor) SetImage(pixels []byte, w, h, hotX, hotY int) error { | |||
| 77 | 92 | ||
| 78 | 93 | // apply (re)attaches the buffer at the current position. | |
| 79 | 94 | func (c *Cursor) apply() error { | |
| 80 | - | if !c.shown { | |
| 95 | + | if !c.shown || c.buf == nil { | |
| 81 | 96 | return nil | |
| 82 | 97 | } | |
| 83 | 98 | v := modeCursor2{flags: cursorBO | cursorMove, crtcID: c.crtc, x: int32(c.x - c.hotX), y: int32(c.y - c.hotY), width: uint32(c.size), height: uint32(c.size), handle: c.buf.handle, hotX: int32(c.hotX), hotY: int32(c.hotY)} | |
| @@ -90,7 +105,7 @@ func (c *Cursor) Move(x, y float64) { | |||
| 90 | 105 | c.mu.Lock() | |
| 91 | 106 | defer c.mu.Unlock() | |
| 92 | 107 | c.x, c.y = int(x), int(y) | |
| 93 | - | if !c.shown { | |
| 108 | + | if !c.shown || c.buf == nil { | |
| 94 | 109 | return | |
| 95 | 110 | } | |
| 96 | 111 | v := modeCursor2{flags: cursorMove, crtcID: c.crtc, x: int32(c.x - c.hotX), y: int32(c.y - c.hotY)} | |
| @@ -108,9 +123,12 @@ func (c *Cursor) Reapply() error { | |||
| 108 | 123 | func (c *Cursor) close() { | |
| 109 | 124 | c.mu.Lock() | |
| 110 | 125 | defer c.mu.Unlock() | |
| 126 | + | if c.buf == nil { | |
| 127 | + | return | |
| 128 | + | } | |
| 111 | 129 | // Best effort: at exit the card may already belong to another session. | |
| 112 | 130 | v := modeCursor2{flags: cursorBO, crtcID: c.crtc} | |
| 113 | 131 | _ = ioctl(c.fd, ioctlCursor2, unsafe.Pointer(&v)) | |
| 114 | 132 | c.buf.destroy(c.fd) | |
| 115 | - | c.shown = false | |
| 133 | + | c.buf, c.shown = nil, false | |
| 116 | 134 | } | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | package drm | 1 | package drm | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "fmt" | |||
| 4 | "sync" | 5 | "sync" | ||
| 5 | "unsafe" | 6 | "unsafe" | ||
| 6 | ) | 7 | ) | ||
| @@ -43,17 +44,28 @@ type Cursor struct { | |||||
| 43 | // newCursor allocates a cursor buffer at the driver's preferred size (64 when | 44 | // newCursor allocates a cursor buffer at the driver's preferred size (64 when | ||
| 44 | // the driver does not say). | 45 | // the driver does not say). | ||
| 45 | func newCursor(fd int, crtc uint32) (*Cursor, error) { | 46 | func newCursor(fd int, crtc uint32) (*Cursor, error) { | ||
| 46 | - | size := 64 | 47 | + | // The plane is square: the smaller of the width and height caps. |
| 48 | + | size := 0 | |||
| 47 | for _, c := range []uint64{capCursorW, capCursorH} { | 49 | for _, c := range []uint64{capCursorW, capCursorH} { | ||
| 48 | v := getCap{capability: c} | 50 | v := getCap{capability: c} | ||
| 49 | - | if ioctl(fd, ioctlGetCap, unsafe.Pointer(&v)) == nil && v.value >= 32 && v.value <= 512 { | 51 | + | if ioctl(fd, ioctlGetCap, unsafe.Pointer(&v)) == nil && v.value >= 16 && v.value <= 512 { |
| 50 | - | size = max(size, int(v.value)) | 52 | + | if size == 0 || int(v.value) < size { |
| 53 | + | size = int(v.value) | |||
| 54 | + | } | |||
| 51 | } | 55 | } | ||
| 52 | } | 56 | } | ||
| 57 | + | if size == 0 { | |||
| 58 | + | size = 64 | |||
| 59 | + | } | |||
| 53 | buf, err := newDumb(fd, size, size) | 60 | buf, err := newDumb(fd, size, size) | ||
| 54 | if err != nil { | 61 | if err != nil { | ||
| 55 | return nil, err | 62 | return nil, err | ||
| 56 | } | 63 | } | ||
| 64 | + | // The cursor ioctl passes no pitch: drivers assume width*4. | |||
| 65 | + | if int(buf.pitch) != size*4 { | |||
| 66 | + | buf.destroy(fd) | |||
| 67 | + | return nil, fmt.Errorf("cursor buffer pitch %d, want %d", buf.pitch, size*4) | |||
| 68 | + | } | |||
| 57 | return &Cursor{fd: fd, crtc: crtc, buf: buf, size: size}, nil | 69 | return &Cursor{fd: fd, crtc: crtc, buf: buf, size: size}, nil | ||
| 58 | } | 70 | } | ||
| 59 | 71 | ||||
| @@ -65,6 +77,9 @@ func (c *Cursor) Limit() int { return c.size } | |||||
| 65 | func (c *Cursor) SetImage(pixels []byte, w, h, hotX, hotY int) error { | 77 | func (c *Cursor) SetImage(pixels []byte, w, h, hotX, hotY int) error { | ||
| 66 | c.mu.Lock() | 78 | c.mu.Lock() | ||
| 67 | defer c.mu.Unlock() | 79 | defer c.mu.Unlock() | ||
| 80 | + | if c.buf == nil { | |||
| 81 | + | return nil | |||
| 82 | + | } | |||
| 68 | pitch := int(c.buf.pitch) | 83 | pitch := int(c.buf.pitch) | ||
| 69 | clear(c.buf.mem) | 84 | clear(c.buf.mem) | ||
| 70 | for y := range min(h, c.size) { | 85 | for y := range min(h, c.size) { | ||
| @@ -77,7 +92,7 @@ func (c *Cursor) SetImage(pixels []byte, w, h, hotX, hotY int) error { | |||||
| 77 | 92 | ||||
| 78 | // apply (re)attaches the buffer at the current position. | 93 | // apply (re)attaches the buffer at the current position. | ||
| 79 | func (c *Cursor) apply() error { | 94 | func (c *Cursor) apply() error { | ||
| 80 | - | if !c.shown { | 95 | + | if !c.shown || c.buf == nil { |
| 81 | return nil | 96 | return nil | ||
| 82 | } | 97 | } | ||
| 83 | v := modeCursor2{flags: cursorBO | cursorMove, crtcID: c.crtc, x: int32(c.x - c.hotX), y: int32(c.y - c.hotY), width: uint32(c.size), height: uint32(c.size), handle: c.buf.handle, hotX: int32(c.hotX), hotY: int32(c.hotY)} | 98 | v := modeCursor2{flags: cursorBO | cursorMove, crtcID: c.crtc, x: int32(c.x - c.hotX), y: int32(c.y - c.hotY), width: uint32(c.size), height: uint32(c.size), handle: c.buf.handle, hotX: int32(c.hotX), hotY: int32(c.hotY)} | ||
| @@ -90,7 +105,7 @@ func (c *Cursor) Move(x, y float64) { | |||||
| 90 | c.mu.Lock() | 105 | c.mu.Lock() | ||
| 91 | defer c.mu.Unlock() | 106 | defer c.mu.Unlock() | ||
| 92 | c.x, c.y = int(x), int(y) | 107 | c.x, c.y = int(x), int(y) | ||
| 93 | - | if !c.shown { | 108 | + | if !c.shown || c.buf == nil { |
| 94 | return | 109 | return | ||
| 95 | } | 110 | } | ||
| 96 | v := modeCursor2{flags: cursorMove, crtcID: c.crtc, x: int32(c.x - c.hotX), y: int32(c.y - c.hotY)} | 111 | v := modeCursor2{flags: cursorMove, crtcID: c.crtc, x: int32(c.x - c.hotX), y: int32(c.y - c.hotY)} | ||
| @@ -108,9 +123,12 @@ func (c *Cursor) Reapply() error { | |||||
| 108 | func (c *Cursor) close() { | 123 | func (c *Cursor) close() { | ||
| 109 | c.mu.Lock() | 124 | c.mu.Lock() | ||
| 110 | defer c.mu.Unlock() | 125 | defer c.mu.Unlock() | ||
| 126 | + | if c.buf == nil { | |||
| 127 | + | return | |||
| 128 | + | } | |||
| 111 | // Best effort: at exit the card may already belong to another session. | 129 | // Best effort: at exit the card may already belong to another session. | ||
| 112 | v := modeCursor2{flags: cursorBO, crtcID: c.crtc} | 130 | v := modeCursor2{flags: cursorBO, crtcID: c.crtc} | ||
| 113 | _ = ioctl(c.fd, ioctlCursor2, unsafe.Pointer(&v)) | 131 | _ = ioctl(c.fd, ioctlCursor2, unsafe.Pointer(&v)) | ||
| 114 | c.buf.destroy(c.fd) | 132 | c.buf.destroy(c.fd) | ||
| 115 | - | c.shown = false | 133 | + | c.buf, c.shown = nil, false |
| 116 | } | 134 | } | ||
M internal/adapters/drm/output.go +1 -1
| @@ -176,7 +176,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (Renderer, | |||
| 176 | 176 | surfaces := make(map[ports.WindowID]ports.SurfaceContent) | |
| 177 | 177 | var scene ports.Scene | |
| 178 | 178 | haveScene, dirty, enabled := false, false, true | |
| 179 | - | cursorScale := 0.0 | |
| 179 | + | cursorScale := -1.0 // not loaded yet | |
| 180 | 180 | frame := 0 | |
| 181 | 181 | stats := time.NewTicker(10 * time.Second) | |
| 182 | 182 | defer stats.Stop() | |
| @@ -176,7 +176,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (Renderer, | |||||
| 176 | surfaces := make(map[ports.WindowID]ports.SurfaceContent) | 176 | surfaces := make(map[ports.WindowID]ports.SurfaceContent) | ||
| 177 | var scene ports.Scene | 177 | var scene ports.Scene | ||
| 178 | haveScene, dirty, enabled := false, false, true | 178 | haveScene, dirty, enabled := false, false, true | ||
| 179 | - | cursorScale := 0.0 | 179 | + | cursorScale := -1.0 // not loaded yet |
| 180 | frame := 0 | 180 | frame := 0 | ||
| 181 | stats := time.NewTicker(10 * time.Second) | 181 | stats := time.NewTicker(10 * time.Second) | ||
| 182 | defer stats.Stop() | 182 | defer stats.Stop() | ||
M internal/adapters/headless/cursor.go +4 -3
| @@ -47,9 +47,10 @@ func (c *Cursor) draw(dst *image.RGBA) { | |||
| 47 | 47 | } | |
| 48 | 48 | d := dst.Pix[dst.PixOffset(dx, dy):] | |
| 49 | 49 | // Source is premultiplied B,G,R,A; dst is straight R,G,B,A (opaque). | |
| 50 | - | d[0] = byte(int(s[2]) + int(d[0])*(255-a)/255) | |
| 51 | - | d[1] = byte(int(s[1]) + int(d[1])*(255-a)/255) | |
| 52 | - | d[2] = byte(int(s[0]) + int(d[2])*(255-a)/255) | |
| 50 | + | // min guards against themes that are not really premultiplied. | |
| 51 | + | d[0] = byte(min(255, int(s[2])+int(d[0])*(255-a)/255)) | |
| 52 | + | d[1] = byte(min(255, int(s[1])+int(d[1])*(255-a)/255)) | |
| 53 | + | d[2] = byte(min(255, int(s[0])+int(d[2])*(255-a)/255)) | |
| 53 | 54 | } | |
| 54 | 55 | } | |
| 55 | 56 | } | |
| @@ -47,9 +47,10 @@ func (c *Cursor) draw(dst *image.RGBA) { | |||||
| 47 | } | 47 | } | ||
| 48 | d := dst.Pix[dst.PixOffset(dx, dy):] | 48 | d := dst.Pix[dst.PixOffset(dx, dy):] | ||
| 49 | // Source is premultiplied B,G,R,A; dst is straight R,G,B,A (opaque). | 49 | // Source is premultiplied B,G,R,A; dst is straight R,G,B,A (opaque). | ||
| 50 | - | d[0] = byte(int(s[2]) + int(d[0])*(255-a)/255) | 50 | + | // min guards against themes that are not really premultiplied. |
| 51 | - | d[1] = byte(int(s[1]) + int(d[1])*(255-a)/255) | 51 | + | d[0] = byte(min(255, int(s[2])+int(d[0])*(255-a)/255)) |
| 52 | - | d[2] = byte(int(s[0]) + int(d[2])*(255-a)/255) | 52 | + | d[1] = byte(min(255, int(s[1])+int(d[1])*(255-a)/255)) |
| 53 | + | d[2] = byte(min(255, int(s[0])+int(d[2])*(255-a)/255)) | |||
| 53 | } | 54 | } | ||
| 54 | } | 55 | } | ||
| 55 | } | 56 | } | ||
M internal/adapters/headless/output.go +1 -1
| @@ -40,7 +40,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 40 | 40 | var scene ports.Scene | |
| 41 | 41 | haveScene := false | |
| 42 | 42 | frame := 0 | |
| 43 | - | cursorScale := 0.0 | |
| 43 | + | cursorScale := -1.0 // not loaded yet | |
| 44 | 44 | update := func(c ports.SurfaceContent) { | |
| 45 | 45 | if c.Pixels == nil { | |
| 46 | 46 | delete(surfaces, c.ID) | |
| @@ -40,7 +40,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 40 | var scene ports.Scene | 40 | var scene ports.Scene | ||
| 41 | haveScene := false | 41 | haveScene := false | ||
| 42 | frame := 0 | 42 | frame := 0 | ||
| 43 | - | cursorScale := 0.0 | 43 | + | cursorScale := -1.0 // not loaded yet |
| 44 | update := func(c ports.SurfaceContent) { | 44 | update := func(c ports.SurfaceContent) { | ||
| 45 | if c.Pixels == nil { | 45 | if c.Pixels == nil { | ||
| 46 | delete(surfaces, c.ID) | 46 | delete(surfaces, c.ID) | ||
M internal/adapters/xcursor/xcursor.go +6 -4
| @@ -63,6 +63,10 @@ func Decode(data []byte, size int) (Image, error) { | |||
| 63 | 63 | return Image{}, errors.New("truncated image header") | |
| 64 | 64 | } | |
| 65 | 65 | h := data[best:] | |
| 66 | + | // The chunk header must repeat its table-of-contents entry. | |
| 67 | + | if le.Uint32(h) != 36 || le.Uint32(h[4:]) != chunkImage || le.Uint32(h[8:]) != bestSize { | |
| 68 | + | return Image{}, errors.New("image chunk does not match its table entry") | |
| 69 | + | } | |
| 66 | 70 | w, ht := le.Uint32(h[16:]), le.Uint32(h[20:]) | |
| 67 | 71 | if w == 0 || ht == 0 || w > maxSide || ht > maxSide { | |
| 68 | 72 | return Image{}, fmt.Errorf("bad image size %dx%d", w, ht) | |
| @@ -170,10 +174,8 @@ func inherits(dirs []string, theme string) []string { | |||
| 170 | 174 | defer f.Close() | |
| 171 | 175 | s := bufio.NewScanner(f) | |
| 172 | 176 | for s.Scan() { | |
| 173 | - | if v, ok := strings.CutPrefix(strings.TrimSpace(s.Text()), "Inherits"); ok { | |
| 174 | - | if v, ok := strings.CutPrefix(strings.TrimSpace(v), "="); ok { | |
| 175 | - | return strings.FieldsFunc(v, func(r rune) bool { return r == ',' || r == ';' || r == ' ' }) | |
| 176 | - | } | |
| 177 | + | if k, v, ok := strings.Cut(s.Text(), "="); ok && strings.TrimSpace(k) == "Inherits" { | |
| 178 | + | return strings.FieldsFunc(v, func(r rune) bool { return r == ',' || r == ';' || r == ' ' }) | |
| 177 | 179 | } | |
| 178 | 180 | } | |
| 179 | 181 | return nil | |
| @@ -63,6 +63,10 @@ func Decode(data []byte, size int) (Image, error) { | |||||
| 63 | return Image{}, errors.New("truncated image header") | 63 | return Image{}, errors.New("truncated image header") | ||
| 64 | } | 64 | } | ||
| 65 | h := data[best:] | 65 | h := data[best:] | ||
| 66 | + | // The chunk header must repeat its table-of-contents entry. | |||
| 67 | + | if le.Uint32(h) != 36 || le.Uint32(h[4:]) != chunkImage || le.Uint32(h[8:]) != bestSize { | |||
| 68 | + | return Image{}, errors.New("image chunk does not match its table entry") | |||
| 69 | + | } | |||
| 66 | w, ht := le.Uint32(h[16:]), le.Uint32(h[20:]) | 70 | w, ht := le.Uint32(h[16:]), le.Uint32(h[20:]) | ||
| 67 | if w == 0 || ht == 0 || w > maxSide || ht > maxSide { | 71 | if w == 0 || ht == 0 || w > maxSide || ht > maxSide { | ||
| 68 | return Image{}, fmt.Errorf("bad image size %dx%d", w, ht) | 72 | return Image{}, fmt.Errorf("bad image size %dx%d", w, ht) | ||
| @@ -170,10 +174,8 @@ func inherits(dirs []string, theme string) []string { | |||||
| 170 | defer f.Close() | 174 | defer f.Close() | ||
| 171 | s := bufio.NewScanner(f) | 175 | s := bufio.NewScanner(f) | ||
| 172 | for s.Scan() { | 176 | for s.Scan() { | ||
| 173 | - | if v, ok := strings.CutPrefix(strings.TrimSpace(s.Text()), "Inherits"); ok { | 177 | + | if k, v, ok := strings.Cut(s.Text(), "="); ok && strings.TrimSpace(k) == "Inherits" { |
| 174 | - | if v, ok := strings.CutPrefix(strings.TrimSpace(v), "="); ok { | 178 | + | return strings.FieldsFunc(v, func(r rune) bool { return r == ',' || r == ';' || r == ' ' }) |
| 175 | - | return strings.FieldsFunc(v, func(r rune) bool { return r == ',' || r == ';' || r == ' ' }) | |||
| 176 | - | } | |||
| 177 | } | 179 | } | ||
| 178 | } | 180 | } | ||
| 179 | return nil | 181 | return nil | ||
M internal/adapters/xcursor/xcursor_test.go +26 -0
| @@ -101,3 +101,29 @@ func TestSystemTheme(t *testing.T) { | |||
| 101 | 101 | t.Fatal(img.W) | |
| 102 | 102 | } | |
| 103 | 103 | } | |
| 104 | + | ||
| 105 | + | func FuzzDecode(f *testing.F) { | |
| 106 | + | f.Add(file(24, 48), 24) | |
| 107 | + | f.Add(file(32), 64) | |
| 108 | + | f.Fuzz(func(t *testing.T, data []byte, size int) { | |
| 109 | + | img, err := Decode(data, size) | |
| 110 | + | if err == nil && (len(img.Pixels) != img.W*img.H*4 || img.HotX >= img.W || img.HotY >= img.H) { | |
| 111 | + | t.Fatalf("%dx%d hot %d,%d len %d", img.W, img.H, img.HotX, img.HotY, len(img.Pixels)) | |
| 112 | + | } | |
| 113 | + | }) | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | func TestLoadInheritsCycle(t *testing.T) { | |
| 117 | + | dir := t.TempDir() | |
| 118 | + | for theme, parent := range map[string]string{"a": "b", "b": "a"} { | |
| 119 | + | if err := os.MkdirAll(filepath.Join(dir, theme), 0o755); err != nil { | |
| 120 | + | t.Fatal(err) | |
| 121 | + | } | |
| 122 | + | if err := os.WriteFile(filepath.Join(dir, theme, "index.theme"), []byte("[Icon Theme]\nInherits="+parent+"\n"), 0o644); err != nil { | |
| 123 | + | t.Fatal(err) | |
| 124 | + | } | |
| 125 | + | } | |
| 126 | + | if _, err := Load([]string{dir}, "a", []string{"default"}, 24); err == nil { | |
| 127 | + | t.Fatal("found a cursor in an empty cycle") | |
| 128 | + | } | |
| 129 | + | } | |
| @@ -101,3 +101,29 @@ func TestSystemTheme(t *testing.T) { | |||||
| 101 | t.Fatal(img.W) | 101 | t.Fatal(img.W) | ||
| 102 | } | 102 | } | ||
| 103 | } | 103 | } | ||
| 104 | + | ||||
| 105 | + | func FuzzDecode(f *testing.F) { | |||
| 106 | + | f.Add(file(24, 48), 24) | |||
| 107 | + | f.Add(file(32), 64) | |||
| 108 | + | f.Fuzz(func(t *testing.T, data []byte, size int) { | |||
| 109 | + | img, err := Decode(data, size) | |||
| 110 | + | if err == nil && (len(img.Pixels) != img.W*img.H*4 || img.HotX >= img.W || img.HotY >= img.H) { | |||
| 111 | + | t.Fatalf("%dx%d hot %d,%d len %d", img.W, img.H, img.HotX, img.HotY, len(img.Pixels)) | |||
| 112 | + | } | |||
| 113 | + | }) | |||
| 114 | + | } | |||
| 115 | + | ||||
| 116 | + | func TestLoadInheritsCycle(t *testing.T) { | |||
| 117 | + | dir := t.TempDir() | |||
| 118 | + | for theme, parent := range map[string]string{"a": "b", "b": "a"} { | |||
| 119 | + | if err := os.MkdirAll(filepath.Join(dir, theme), 0o755); err != nil { | |||
| 120 | + | t.Fatal(err) | |||
| 121 | + | } | |||
| 122 | + | if err := os.WriteFile(filepath.Join(dir, theme, "index.theme"), []byte("[Icon Theme]\nInherits="+parent+"\n"), 0o644); err != nil { | |||
| 123 | + | t.Fatal(err) | |||
| 124 | + | } | |||
| 125 | + | } | |||
| 126 | + | if _, err := Load([]string{dir}, "a", []string{"default"}, 24); err == nil { | |||
| 127 | + | t.Fatal("found a cursor in an empty cycle") | |||
| 128 | + | } | |||
| 129 | + | } | |||