1package xcursor
2
3import (
4 "encoding/binary"
5 "os"
6 "path/filepath"
7 "testing"
8)
9
10// file builds an Xcursor file with one square image per size; the hotspot is
11// size/8 and every pixel is opaque white.
12func file(sizes ...int) []byte {
13 le := binary.LittleEndian
14 out := le.AppendUint32([]byte("Xcur"), 16)
15 out = le.AppendUint32(out, 0x10000)
16 out = le.AppendUint32(out, uint32(len(sizes)))
17 pos := 16 + 12*len(sizes)
18 for _, s := range sizes {
19 out = le.AppendUint32(out, chunkImage)
20 out = le.AppendUint32(out, uint32(s))
21 out = le.AppendUint32(out, uint32(pos))
22 pos += 36 + s*s*4
23 }
24 for _, s := range sizes {
25 for _, v := range []int{36, chunkImage, s, 1, s, s, s / 8, s / 8, 0} {
26 out = le.AppendUint32(out, uint32(v))
27 }
28 for range s * s {
29 out = append(out, 255, 255, 255, 255)
30 }
31 }
32 return out
33}
34
35func TestDecodePicksSize(t *testing.T) {
36 data := file(24, 36, 48)
37 for _, tc := range []struct{ want, got int }{{24, 24}, {30, 36}, {36, 36}, {40, 48}, {96, 48}, {1, 24}} {
38 img, err := Decode(data, tc.want)
39 if err != nil || img.W != tc.got || img.HotX != tc.got/8 || len(img.Pixels) != tc.got*tc.got*4 {
40 t.Errorf("size %d: %+v %v", tc.want, img.W, err)
41 }
42 }
43}
44
45func TestDecodeRejectsBadFiles(t *testing.T) {
46 good := file(24)
47 for name, data := range map[string][]byte{
48 "empty": nil,
49 "magic": append([]byte("Xcux"), good[4:]...),
50 "toc": good[:20],
51 "pixels": good[:len(good)-4],
52 "no images": file(),
53 } {
54 if _, err := Decode(data, 24); err == nil {
55 t.Errorf("%s: no error", name)
56 }
57 }
58 // A huge declared size must not allocate.
59 huge := file(24)
60 binary.LittleEndian.PutUint32(huge[16+12+16:], 1<<20)
61 if _, err := Decode(huge, 24); err == nil {
62 t.Error("huge image accepted")
63 }
64}
65
66func TestScale(t *testing.T) {
67 img, _ := Decode(file(48), 48)
68 s := img.Scale(36, 36)
69 if s.W != 36 || s.H != 36 || s.HotX != 4 || len(s.Pixels) != 36*36*4 || s.Pixels[0] != 255 {
70 t.Fatalf("%+v", s.W)
71 }
72}
73
74func TestLoadFollowsInherits(t *testing.T) {
75 dir := t.TempDir()
76 must := func(err error) {
77 t.Helper()
78 if err != nil {
79 t.Fatal(err)
80 }
81 }
82 must(os.MkdirAll(filepath.Join(dir, "child"), 0o755))
83 must(os.WriteFile(filepath.Join(dir, "child", "index.theme"), []byte("[Icon Theme]\nInherits=parent\n"), 0o644))
84 must(os.MkdirAll(filepath.Join(dir, "parent", "cursors"), 0o755))
85 must(os.WriteFile(filepath.Join(dir, "parent", "cursors", "left_ptr"), file(24), 0o644))
86 img, err := Load([]string{dir}, "child", []string{"default", "left_ptr"}, 24)
87 if err != nil || img.W != 24 {
88 t.Fatal(img.W, err)
89 }
90 if _, err := Load([]string{dir}, "child", []string{"nope"}, 24); err == nil {
91 t.Fatal("missing cursor found")
92 }
93}
94
95func TestSystemTheme(t *testing.T) {
96 img, err := Load(SearchPath(), os.Getenv("XCURSOR_THEME"), []string{"default", "left_ptr"}, 24)
97 if err != nil {
98 t.Skip("no cursor theme installed")
99 }
100 if img.W < 16 || img.W > 64 {
101 t.Fatal(img.W)
102 }
103}
104
105func 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
116func 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}