1/*
2Package gbytes provides a buffer that supports incrementally detecting input.
3
4You use gbytes.Buffer with the gbytes.Say matcher.  When Say finds a match, it fastforwards the buffer's read cursor to the end of that match.
5
6Subsequent matches against the buffer will only operate against data that appears *after* the read cursor.
7
8The read cursor is an opaque implementation detail that you cannot access.  You should use the Say matcher to sift through the buffer.  You can always
9access the entire buffer's contents with Contents().
10
11*/
12package gbytes
13
14import (
15	"errors"
16	"fmt"
17	"io"
18	"regexp"
19	"sync"
20	"time"
21)
22
23/*
24gbytes.Buffer implements an io.Writer and can be used with the gbytes.Say matcher.
25
26You should only use a gbytes.Buffer in test code.  It stores all writes in an in-memory buffer - behavior that is inappropriate for production code!
27*/
28type Buffer struct {
29	contents     []byte
30	readCursor   uint64
31	lock         *sync.Mutex
32	detectCloser chan interface{}
33	closed       bool
34}
35
36/*
37NewBuffer returns a new gbytes.Buffer
38*/
39func NewBuffer() *Buffer {
40	return &Buffer{
41		lock: &sync.Mutex{},
42	}
43}
44
45/*
46BufferWithBytes returns a new gbytes.Buffer seeded with the passed in bytes
47*/
48func BufferWithBytes(bytes []byte) *Buffer {
49	return &Buffer{
50		lock:     &sync.Mutex{},
51		contents: bytes,
52	}
53}
54
55/*
56BufferReader returns a new gbytes.Buffer that wraps a reader.  The reader's contents are read into
57the Buffer via io.Copy
58*/
59func BufferReader(reader io.Reader) *Buffer {
60	b := &Buffer{
61		lock: &sync.Mutex{},
62	}
63
64	go func() {
65		io.Copy(b, reader)
66		b.Close()
67	}()
68
69	return b
70}
71
72/*
73Write implements the io.Writer interface
74*/
75func (b *Buffer) Write(p []byte) (n int, err error) {
76	b.lock.Lock()
77	defer b.lock.Unlock()
78
79	if b.closed {
80		return 0, errors.New("attempt to write to closed buffer")
81	}
82
83	b.contents = append(b.contents, p...)
84	return len(p), nil
85}
86
87/*
88Read implements the io.Reader interface. It advances the
89cursor as it reads.
90
91Returns an error if called after Close.
92*/
93func (b *Buffer) Read(d []byte) (int, error) {
94	b.lock.Lock()
95	defer b.lock.Unlock()
96
97	if b.closed {
98		return 0, errors.New("attempt to read from closed buffer")
99	}
100
101	if uint64(len(b.contents)) <= b.readCursor {
102		return 0, io.EOF
103	}
104
105	n := copy(d, b.contents[b.readCursor:])
106	b.readCursor += uint64(n)
107
108	return n, nil
109}
110
111/*
112Close signifies that the buffer will no longer be written to
113*/
114func (b *Buffer) Close() error {
115	b.lock.Lock()
116	defer b.lock.Unlock()
117
118	b.closed = true
119
120	return nil
121}
122
123/*
124Closed returns true if the buffer has been closed
125*/
126func (b *Buffer) Closed() bool {
127	b.lock.Lock()
128	defer b.lock.Unlock()
129
130	return b.closed
131}
132
133/*
134Contents returns all data ever written to the buffer.
135*/
136func (b *Buffer) Contents() []byte {
137	b.lock.Lock()
138	defer b.lock.Unlock()
139
140	contents := make([]byte, len(b.contents))
141	copy(contents, b.contents)
142	return contents
143}
144
145/*
146Detect takes a regular expression and returns a channel.
147
148The channel will receive true the first time data matching the regular expression is written to the buffer.
149The channel is subsequently closed and the buffer's read-cursor is fast-forwarded to just after the matching region.
150
151You typically don't need to use Detect and should use the ghttp.Say matcher instead.  Detect is useful, however, in cases where your code must
152be branch and handle different outputs written to the buffer.
153
154For example, consider a buffer hooked up to the stdout of a client library.  You may (or may not, depending on state outside of your control) need to authenticate the client library.
155
156You could do something like:
157
158select {
159case <-buffer.Detect("You are not logged in"):
160	//log in
161case <-buffer.Detect("Success"):
162	//carry on
163case <-time.After(time.Second):
164	//welp
165}
166buffer.CancelDetects()
167
168You should always call CancelDetects after using Detect.  This will close any channels that have not detected and clean up the goroutines that were spawned to support them.
169
170Finally, you can pass detect a format string followed by variadic arguments.  This will construct the regexp using fmt.Sprintf.
171*/
172func (b *Buffer) Detect(desired string, args ...interface{}) chan bool {
173	formattedRegexp := desired
174	if len(args) > 0 {
175		formattedRegexp = fmt.Sprintf(desired, args...)
176	}
177	re := regexp.MustCompile(formattedRegexp)
178
179	b.lock.Lock()
180	defer b.lock.Unlock()
181
182	if b.detectCloser == nil {
183		b.detectCloser = make(chan interface{})
184	}
185
186	closer := b.detectCloser
187	response := make(chan bool)
188	go func() {
189		ticker := time.NewTicker(10 * time.Millisecond)
190		defer ticker.Stop()
191		defer close(response)
192		for {
193			select {
194			case <-ticker.C:
195				b.lock.Lock()
196				data, cursor := b.contents[b.readCursor:], b.readCursor
197				loc := re.FindIndex(data)
198				b.lock.Unlock()
199
200				if loc != nil {
201					response <- true
202					b.lock.Lock()
203					newCursorPosition := cursor + uint64(loc[1])
204					if newCursorPosition >= b.readCursor {
205						b.readCursor = newCursorPosition
206					}
207					b.lock.Unlock()
208					return
209				}
210			case <-closer:
211				return
212			}
213		}
214	}()
215
216	return response
217}
218
219/*
220CancelDetects cancels any pending detects and cleans up their goroutines.  You should always call this when you're done with a set of Detect channels.
221*/
222func (b *Buffer) CancelDetects() {
223	b.lock.Lock()
224	defer b.lock.Unlock()
225
226	close(b.detectCloser)
227	b.detectCloser = nil
228}
229
230func (b *Buffer) didSay(re *regexp.Regexp) (bool, []byte) {
231	b.lock.Lock()
232	defer b.lock.Unlock()
233
234	unreadBytes := b.contents[b.readCursor:]
235	copyOfUnreadBytes := make([]byte, len(unreadBytes))
236	copy(copyOfUnreadBytes, unreadBytes)
237
238	loc := re.FindIndex(unreadBytes)
239
240	if loc != nil {
241		b.readCursor += uint64(loc[1])
242		return true, copyOfUnreadBytes
243	}
244	return false, copyOfUnreadBytes
245}
246