1package xturbo
2
3import (
4 "encoding/binary"
5 "io"
6 "os"
7 "sync"
8 "sync/atomic"
9 "testing"
10)
11
12// scriptExt is an extension the scripted server advertises.
13type scriptExt struct{ major, firstEvent, firstError uint8 }
14
15// script is the behaviour of the scripted X server behind a socketpair. It
16// answers QueryExtension, InternAtom, GetInputFocus, BIG-REQUESTS Enable and
17// XC-MISC GetXIDRange like an X server; other requests get no reply unless
18// hook handles them.
19type script struct {
20 exts map[string]scriptExt
21 bigreqMax uint32 // Enable reply maximum_request_length
22 atoms map[string]uint32 // known atoms; new names get nextAtom++
23 nextAtom uint32
24 badAtoms map[string]bool // InternAtom answers BadAlloc
25 xidRanges [][2]uint32 // successive GetXIDRange replies {start, count}
26 // hook sees every request first; returning true skips the default.
27 hook func(s *scriptConn, major, minor uint8, seq uint16, body []byte) bool
28 // silent, once set, makes the server read requests and never answer
29 // (alive but hung).
30 silent atomic.Bool
31}
32
33// scriptConn is the server side of one scripted connection.
34type scriptConn struct {
35 f *os.File
36 sc *script
37 seq uint16
38
39 mu sync.Mutex
40 reqs []scriptReq // every request after setup, in order
41}
42
43type scriptReq struct {
44 seq uint16
45 major uint8
46 minor uint8
47 raw []byte // whole request, header included
48}
49
50func (s *scriptConn) requests() []scriptReq {
51 s.mu.Lock()
52 defer s.mu.Unlock()
53 return append([]scriptReq(nil), s.reqs...)
54}
55
56func (s *scriptConn) write(p []byte) { _, _ = s.f.Write(p) }
57
58func (s *scriptConn) reply(data uint8, body []byte) {
59 words := (max(len(body), 24) - 24 + 3) / 4
60 p := make([]byte, 32+4*words)
61 p[0], p[1] = 1, data
62 binary.LittleEndian.PutUint16(p[2:], s.seq)
63 binary.LittleEndian.PutUint32(p[4:], uint32(words))
64 copy(p[8:], body)
65 s.write(p)
66}
67
68func (s *scriptConn) xerror(code, major uint8, bad uint32) {
69 p := make([]byte, 32)
70 p[1] = code
71 binary.LittleEndian.PutUint16(p[2:], s.seq)
72 binary.LittleEndian.PutUint32(p[4:], bad)
73 p[10] = major
74 s.write(p)
75}
76
77// event writes a 32-byte core event carrying the current sequence number.
78func (s *scriptConn) event(code uint8, fill byte) {
79 p := make([]byte, 32)
80 for i := range p {
81 p[i] = fill
82 }
83 p[0] = code
84 binary.LittleEndian.PutUint16(p[2:], s.seq)
85 s.write(p)
86}
87
88func (sc *script) extByMajor(major uint8) string {
89 for name, e := range sc.exts {
90 if e.major == major {
91 return name
92 }
93 }
94 return ""
95}
96
97// serve reads requests until EOF and answers them.
98func (s *scriptConn) serve() {
99 sc := s.sc
100 for {
101 var hdr [4]byte
102 if _, err := io.ReadFull(s.f, hdr[:]); err != nil {
103 return
104 }
105 raw := hdr[:]
106 n := int(binary.LittleEndian.Uint16(hdr[2:])) * 4
107 if n == 0 { // BIG-REQUESTS header
108 var l [4]byte
109 if _, err := io.ReadFull(s.f, l[:]); err != nil {
110 return
111 }
112 raw = append(raw, l[:]...)
113 n = int(binary.LittleEndian.Uint32(l[:])) * 4
114 }
115 rest := make([]byte, n-len(raw))
116 if _, err := io.ReadFull(s.f, rest); err != nil {
117 return
118 }
119 raw = append(append([]byte(nil), raw...), rest...)
120 body := raw[len(raw)-len(rest):]
121 s.seq++
122 major, minor := hdr[0], hdr[1]
123 s.mu.Lock()
124 s.reqs = append(s.reqs, scriptReq{s.seq, major, minor, raw})
125 s.mu.Unlock()
126 if sc.silent.Load() || sc.hook != nil && sc.hook(s, major, minor, s.seq, body) {
127 continue
128 }
129 s.answer(major, minor, body)
130 }
131}
132
133// answer runs the default behaviour for one request, holding s.mu so tests
134// can read sc (atom) concurrently.
135func (s *scriptConn) answer(major, minor uint8, body []byte) {
136 sc := s.sc
137 s.mu.Lock()
138 defer s.mu.Unlock()
139 switch {
140 case major == opQueryExtension:
141 e, ok := sc.exts[string(body[4:4+binary.LittleEndian.Uint16(body)])]
142 var present uint8
143 if ok {
144 present = 1
145 }
146 s.reply(0, []byte{present, e.major, e.firstEvent, e.firstError})
147 case major == opInternAtom:
148 name := string(body[4 : 4+binary.LittleEndian.Uint16(body)])
149 if sc.badAtoms[name] {
150 s.xerror(11, opInternAtom, 0) // BadAlloc
151 return
152 }
153 a, ok := sc.atoms[name]
154 if !ok && minor == 0 {
155 if sc.atoms == nil {
156 sc.atoms = map[string]uint32{}
157 }
158 sc.nextAtom++
159 a = sc.nextAtom + 1000
160 sc.atoms[name] = a
161 }
162 s.reply(0, binary.LittleEndian.AppendUint32(nil, a))
163 case major == opGetInputFocus:
164 s.reply(1, binary.LittleEndian.AppendUint32(nil, 0x1234))
165 case major >= 128 && sc.extByMajor(major) == "BIG-REQUESTS" && minor == 0:
166 s.reply(0, binary.LittleEndian.AppendUint32(nil, sc.bigreqMax))
167 case major >= 128 && sc.extByMajor(major) == "XC-MISC" && minor == minorGetXIDRange:
168 r := [2]uint32{0, 1}
169 if len(sc.xidRanges) > 0 {
170 r, sc.xidRanges = sc.xidRanges[0], sc.xidRanges[1:]
171 }
172 s.reply(0, binary.LittleEndian.AppendUint32(binary.LittleEndian.AppendUint32(nil, r[0]), r[1]))
173 }
174}
175
176// fullExts is what a typical Xvfb advertises, at its usual opcodes.
177func fullExts() map[string]scriptExt {
178 return map[string]scriptExt{
179 "BIG-REQUESTS": {133, 0, 0},
180 "Generic Event Extension": {128, 0, 0},
181 "XC-MISC": {136, 0, 0},
182 "Composite": {142, 0, 0},
183 "XFIXES": {138, 87, 140},
184 "X-Resource": {144, 0, 0},
185 "RENDER": {139, 0, 141},
186 "SHAPE": {129, 64, 0},
187 }
188}
189
190// startScript serves sc behind a socketpair after answering the setup with
191// the recorded Success reply, and returns the client fd (not yet set up) and
192// the server connection.
193func startScript(t *testing.T, sc *script) (int, *scriptConn) {
194 t.Helper()
195 fd, srv, _ := serveSetup(t, loadSetup(t, "Success"), 0, false, sc)
196 return fd, srv
197}
198
199// dialScript runs NewConn against sc.
200func dialScript(t *testing.T, sc *script) (*Conn, *scriptConn) {
201 t.Helper()
202 fd, srv := startScript(t, sc)
203 c, err := NewConn(fd)
204 if err != nil {
205 t.Fatal(err)
206 }
207 t.Cleanup(func() { _ = c.Close() })
208 return c, srv
209}
210
211// atom returns the atom the server assigned to name.
212func (s *scriptConn) atom(name string) uint32 {
213 s.mu.Lock()
214 defer s.mu.Unlock()
215 return s.sc.atoms[name]
216}