1package glyph
2
3import (
4 "encoding/binary"
5 "os"
6 "strings"
7 "testing"
8)
9
10// nerdFont returns the path of an installed Nerd Font, or skips the test.
11func nerdFont(t *testing.T) string {
12 t.Helper()
13 paths, err := FindFonts("JetBrainsMono Nerd Font Mono")
14 if err != nil || !strings.Contains(strings.ToLower(paths[0]), "nerd") {
15 t.Skip("no Nerd Font installed")
16 }
17 return paths[0]
18}
19
20func TestNamesLookup(t *testing.T) {
21 n, err := LoadNames(nerdFont(t))
22 if err != nil {
23 t.Fatal(err)
24 }
25 defer n.Close()
26 if n.Len() < 1000 {
27 t.Fatalf("only %d names indexed", n.Len())
28 }
29 for name, want := range map[string]rune{"fa-steam": 0xF1B6, "md-steam": 0xF04D3} {
30 if r, ok := n.Lookup(name); !ok || r != want {
31 t.Errorf("Lookup(%q) = %U, %v; want %U", name, r, ok, want)
32 }
33 }
34 if _, ok := n.Lookup("no-such-glyph"); ok {
35 t.Error("an unknown name was found")
36 }
37 if allocs := testing.AllocsPerRun(100, func() { n.Lookup("fa-steam") }); allocs != 0 {
38 t.Errorf("Lookup allocates %v times", allocs)
39 }
40}
41
42func TestNilNamesLookup(t *testing.T) {
43 var n *Names
44 if _, ok := n.Lookup("fa-steam"); ok {
45 t.Error("a nil index found a name")
46 }
47}
48
49// postFont builds a font file holding only a "post" table of the given
50// version, with one glyph index per entry of indices and the custom names
51// after them.
52func postFont(version uint32, indices []uint16, names ...string) []byte {
53 post := make([]byte, 34+2*len(indices))
54 binary.BigEndian.PutUint32(post, version)
55 binary.BigEndian.PutUint16(post[32:], uint16(len(indices)))
56 for i, k := range indices {
57 binary.BigEndian.PutUint16(post[34+2*i:], k)
58 }
59 for _, s := range names {
60 post = append(post, byte(len(s)))
61 post = append(post, s...)
62 }
63 b := make([]byte, 12+16, 12+16+len(post))
64 b[5] = 1 // one table
65 copy(b[12:], "post")
66 binary.BigEndian.PutUint32(b[12+8:], 28)
67 binary.BigEndian.PutUint32(b[12+12:], uint32(len(post)))
68 return append(b, post...)
69}
70
71func TestPostNames(t *testing.T) {
72 // Glyph 0 has a standard name, 1 and 2 custom ones, 3 a custom index
73 // past the names.
74 data := postFont(0x00020000, []uint16{3, 259, 258, 300}, "first", "second")
75 spans := postNames(data)
76 if len(spans) != 4 {
77 t.Fatalf("%d spans, want 4", len(spans))
78 }
79 name := func(g int) string { return string(data[spans[g].off : spans[g].off+spans[g].n]) }
80 if spans[0].n != 0 || spans[3].n != 0 {
81 t.Errorf("standard or missing names gave spans: %v", spans)
82 }
83 if name(1) != "second" || name(2) != "first" {
84 t.Errorf("names = %q, %q", name(1), name(2))
85 }
86}
87
88func TestPostNamesVersion3(t *testing.T) {
89 // A version 3 table stores no names: nothing to index, no error.
90 if spans := postNames(postFont(0x00030000, []uint16{259}, "x")); spans != nil {
91 t.Fatalf("got %d spans", len(spans))
92 }
93}
94
95func TestPostNamesDamagedFont(t *testing.T) {
96 // Truncated or garbage files give no names and never panic.
97 var good []byte
98 if paths, err := FindFonts("monospace"); err == nil {
99 good, _ = os.ReadFile(paths[0])
100 }
101 inputs := [][]byte{nil, {0, 1, 0, 0}, make([]byte, 64)}
102 for _, cut := range []int{12, 100, 1000, len(good) / 2} {
103 if cut <= len(good) {
104 inputs = append(inputs, good[:cut])
105 }
106 }
107 for _, b := range inputs {
108 _ = postNames(b)
109 }
110 // A "post" table that claims more glyphs than it holds.
111 b := postFont(0x00020000, nil)
112 binary.BigEndian.PutUint16(b[28+32:], 0xff)
113 if spans := postNames(b); spans != nil {
114 t.Fatalf("got %d spans from a bad table", len(spans))
115 }
116}
117
118func FuzzPostNames(f *testing.F) {
119 f.Add(postFont(0x00020000, []uint16{3, 259, 258, 300}, "first", "second"))
120 f.Add(postFont(0x00030000, nil))
121 f.Add([]byte{0, 1, 0, 0})
122 f.Fuzz(func(t *testing.T, data []byte) {
123 for g, s := range postNames(data) {
124 if uint64(s.off)+uint64(s.n) > uint64(len(data)) {
125 t.Fatalf("glyph %d: span %v past %d bytes", g, s, len(data))
126 }
127 }
128 })
129}