okami.mom/ultraviolet

git clone https://git.nkp.pet/okami.mom/ultraviolet

Boxiu Leefix: infinite loop in transformLine when wide char trailing cell is zero (#109)489999b

main
2.6 KiB102 linesraw
1package uv
2
3import (
4	"image/color"
5	"testing"
6	"time"
7)
8
9// recursiveColor is a color.Color implementation whose underlying struct
10// contains an interface field, triggering infinite recursion in
11// runtime.ifaceeq when compared with ==.
12//
13// This reproduces the bug where Cell.IsZero() and Style.IsZero() use
14// struct == comparison on types containing color.Color interface fields.
15type recursiveColor struct {
16	color.Color // embedded interface — ifaceeq recurses into this
17	r, g, b, a  uint8
18}
19
20func (c *recursiveColor) RGBA() (uint32, uint32, uint32, uint32) {
21	return uint32(c.r), uint32(c.g), uint32(c.b), uint32(c.a)
22}
23
24// TestCellIsZero_RecursiveColorPanicsOrHangs tests that Cell.IsZero()
25// does not hang when the cell's style contains a color.Color implementation
26// with an embedded interface.
27//
28// Before the fix, this test would hang forever (CPU 100%) because:
29//
30//	Cell.IsZero() → *c == Cell{}
31//	  → .eq.Cell → .eq.Style → runtime.ifaceeq(color.Color, nil)
32//	    → recurse into embedded interface → infinite loop
33func TestCellIsZero_RecursiveColorPanicsOrHangs(t *testing.T) {
34	// Use a concrete non-nil color with embedded interface
35	c := &Cell{
36		Content: "x",
37		Width:   1,
38		Style: Style{
39			Fg: &recursiveColor{r: 255, g: 0, b: 0, a: 255},
40		},
41	}
42
43	done := make(chan struct{})
44	go func() {
45		defer func() {
46			recover() // in case it panics instead of hanging
47			close(done)
48		}()
49		_ = c.IsZero()
50	}()
51
52	select {
53	case <-done:
54		// Good: returned without hanging
55	case <-time.After(2 * time.Second):
56		t.Fatal("Cell.IsZero() hung — likely infinite recursion in runtime.ifaceeq")
57	}
58}
59
60// TestStyleIsZero_RecursiveColorPanicsOrHangs tests the same issue for Style.IsZero().
61func TestStyleIsZero_RecursiveColorPanicsOrHangs(t *testing.T) {
62	s := &Style{
63		Fg: &recursiveColor{r: 255, g: 0, b: 0, a: 255},
64	}
65
66	done := make(chan struct{})
67	go func() {
68		defer func() {
69			recover()
70			close(done)
71		}()
72		_ = s.IsZero()
73	}()
74
75	select {
76	case <-done:
77	case <-time.After(2 * time.Second):
78		t.Fatal("Style.IsZero() hung — likely infinite recursion in runtime.ifaceeq")
79	}
80}
81
82// TestCellIsZero_NilColorDoesNotHang verifies the common case still works.
83func TestCellIsZero_NilColorDoesNotHang(t *testing.T) {
84	tests := []struct {
85		name string
86		cell *Cell
87		want bool
88	}{
89		{"zero cell", &Cell{}, true},
90		{"nil style colors", &Cell{Content: "x", Width: 1}, false},
91		{"empty content with nil colors", &Cell{Content: " ", Width: 1}, false},
92	}
93
94	for _, tt := range tests {
95		t.Run(tt.name, func(t *testing.T) {
96			got := tt.cell.IsZero()
97			if got != tt.want {
98				t.Errorf("IsZero() = %v, want %v", got, tt.want)
99			}
100		})
101	}
102}