-
-
Notifications
You must be signed in to change notification settings - Fork 17
/
parser.go
393 lines (315 loc) · 8.45 KB
/
parser.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
package irc
import (
"bytes"
"errors"
"strings"
)
var tagDecodeSlashMap = map[rune]rune{
':': ';',
's': ' ',
'\\': '\\',
'r': '\r',
'n': '\n',
}
var tagEncodeMap = map[rune]string{
';': "\\:",
' ': "\\s",
'\\': "\\\\",
'\r': "\\r",
'\n': "\\n",
}
var (
// ErrZeroLengthMessage is returned when parsing if the input is
// zero-length.
ErrZeroLengthMessage = errors.New("irc: cannot parse zero-length message")
// ErrMissingDataAfterPrefix is returned when parsing if there is
// no message data after the prefix.
ErrMissingDataAfterPrefix = errors.New("irc: no message data after prefix")
// ErrMissingDataAfterTags is returned when parsing if there is no
// message data after the tags.
ErrMissingDataAfterTags = errors.New("irc: no message data after tags")
// ErrMissingCommand is returned when parsing if there is no
// command in the parsed message.
ErrMissingCommand = errors.New("irc: missing message command")
)
// ParseTagValue parses an encoded tag value as a string. If you need to set a
// tag, you probably want to just set the string itself, so it will be encoded
// properly.
func ParseTagValue(v string) string {
ret := &bytes.Buffer{}
input := bytes.NewBufferString(v)
for {
c, _, err := input.ReadRune()
if err != nil {
break
}
if c == '\\' {
// If we got a backslash followed by the end of the tag value, we
// should just ignore the backslash.
c2, _, err := input.ReadRune()
if err != nil {
break
}
if replacement, ok := tagDecodeSlashMap[c2]; ok {
ret.WriteRune(replacement)
} else {
ret.WriteRune(c2)
}
} else {
ret.WriteRune(c)
}
}
return ret.String()
}
// EncodeTagValue converts a raw string to the format in the connection.
func EncodeTagValue(v string) string {
ret := &bytes.Buffer{}
for _, c := range v {
if replacement, ok := tagEncodeMap[c]; ok {
ret.WriteString(replacement)
} else {
ret.WriteRune(c)
}
}
return ret.String()
}
// Tags represents the IRCv3 message tags.
type Tags map[string]string
// ParseTags takes a tag string and parses it into a tag map. It will
// always return a tag map, even if there are no valid tags.
func ParseTags(line string) Tags {
ret := Tags{}
tags := strings.Split(line, ";")
for _, tag := range tags {
parts := strings.SplitN(tag, "=", 2)
if len(parts) < 2 {
ret[parts[0]] = ""
continue
}
ret[parts[0]] = ParseTagValue(parts[1])
}
return ret
}
// Copy will create a new copy of all IRC tags attached to this
// message.
func (t Tags) Copy() Tags {
ret := Tags{}
for k, v := range t {
ret[k] = v
}
return ret
}
// String ensures this is stringable.
func (t Tags) String() string {
buf := &bytes.Buffer{}
for k, v := range t {
buf.WriteByte(';')
buf.WriteString(k)
if v != "" {
buf.WriteByte('=')
buf.WriteString(EncodeTagValue(v))
}
}
// We don't need the first byte because that's an extra ';'
// character.
_, _ = buf.ReadByte()
return buf.String()
}
// Prefix represents the prefix of a message, generally the user who sent it.
type Prefix struct {
// Name will contain the nick of who sent the message, the
// server who sent the message, or a blank string
Name string
// User will either contain the user who sent the message or a blank string
User string
// Host will either contain the host of who sent the message or a blank string
Host string
}
// ParsePrefix takes an identity string and parses it into an
// identity struct. It will always return an Prefix struct and never
// nil.
func ParsePrefix(line string) *Prefix {
// Start by creating an Prefix with nothing but the host
id := &Prefix{
Name: line,
}
uh := strings.SplitN(id.Name, "@", 2)
if len(uh) == 2 {
id.Name, id.Host = uh[0], uh[1]
}
nu := strings.SplitN(id.Name, "!", 2)
if len(nu) == 2 {
id.Name, id.User = nu[0], nu[1]
}
return id
}
// Copy will create a new copy of an Prefix.
func (p *Prefix) Copy() *Prefix {
if p == nil {
return nil
}
newPrefix := &Prefix{}
*newPrefix = *p
return newPrefix
}
// String ensures this is stringable.
func (p *Prefix) String() string {
buf := &bytes.Buffer{}
buf.WriteString(p.Name)
if p.User != "" {
buf.WriteString("!")
buf.WriteString(p.User)
}
if p.Host != "" {
buf.WriteString("@")
buf.WriteString(p.Host)
}
return buf.String()
}
// Message represents a line parsed from the server.
type Message struct {
// Each message can have IRCv3 tags
Tags
// Each message can have a Prefix
*Prefix
// Command is which command is being called.
Command string
// Params are all the arguments for the command.
Params []string
}
// MustParseMessage calls ParseMessage and either returns the message
// or panics if an error is returned.
func MustParseMessage(line string) *Message {
m, err := ParseMessage(line)
if err != nil {
panic(err.Error())
}
return m
}
// ParseMessage takes a message string (usually a whole line) and
// parses it into a Message struct. This will return nil in the case
// of invalid messages.
func ParseMessage(line string) (*Message, error) { //nolint:funlen
// Trim the line and make sure we have data
line = strings.TrimRight(line, "\r\n")
if len(line) == 0 {
return nil, ErrZeroLengthMessage
}
c := &Message{
Tags: Tags{},
Prefix: &Prefix{},
}
if line[0] == '@' {
loc := strings.Index(line, " ")
if loc == -1 {
return nil, ErrMissingDataAfterTags
}
c.Tags = ParseTags(line[1:loc])
line = line[loc+1:]
}
if line[0] == ':' {
loc := strings.Index(line, " ")
if loc == -1 {
return nil, ErrMissingDataAfterPrefix
}
// Parse the identity, if there was one
c.Prefix = ParsePrefix(line[1:loc])
line = line[loc+1:]
}
// Split out the trailing then the rest of the args. Because
// we expect there to be at least one result as an arg (the
// command) we don't need to special case the trailing arg and
// can just attempt a split on " :"
split := strings.SplitN(line, " :", 2)
c.Params = strings.FieldsFunc(split[0], func(r rune) bool {
return r == ' '
})
// If there are no args, we need to bail because we need at
// least the command.
if len(c.Params) == 0 {
return nil, ErrMissingCommand
}
// If we had a trailing arg, append it to the other args
if len(split) == 2 {
c.Params = append(c.Params, split[1])
}
// Because of how it's parsed, the Command will show up as the
// first arg.
c.Command = strings.ToUpper(c.Params[0])
c.Params = c.Params[1:]
// If there are no params, set it to nil, to make writing tests and other
// things simpler.
if len(c.Params) == 0 {
c.Params = nil
}
return c, nil
}
// Param returns the i'th argument in the Message or an empty string
// if the requested arg does not exist.
func (m *Message) Param(i int) string {
if i < 0 || i >= len(m.Params) {
return ""
}
return m.Params[i]
}
// Trailing returns the last argument in the Message or an empty string
// if there are no args.
func (m *Message) Trailing() string {
if len(m.Params) < 1 {
return ""
}
return m.Params[len(m.Params)-1]
}
// Copy will create a new copy of an message.
func (m *Message) Copy() *Message {
// Create a new message
newMessage := &Message{}
// Copy stuff from the old message
*newMessage = *m
// Copy any IRcv3 tags
newMessage.Tags = m.Tags.Copy()
// Copy the Prefix
newMessage.Prefix = m.Prefix.Copy()
// Copy the Params slice
newMessage.Params = append(make([]string, 0, len(m.Params)), m.Params...)
// Similar to parsing, if Params is empty, set it to nil
if len(newMessage.Params) == 0 {
newMessage.Params = nil
}
return newMessage
}
// String ensures this is stringable.
func (m *Message) String() string {
buf := &bytes.Buffer{}
// Write any IRCv3 tags if they exist in the message
if len(m.Tags) > 0 {
buf.WriteByte('@')
buf.WriteString(m.Tags.String())
buf.WriteByte(' ')
}
// Add the prefix if we have one
if m.Prefix != nil && m.Prefix.Name != "" {
buf.WriteByte(':')
buf.WriteString(m.Prefix.String())
buf.WriteByte(' ')
}
// Add the command since we know we'll always have one
buf.WriteString(m.Command)
if len(m.Params) > 0 {
args := m.Params[:len(m.Params)-1]
trailing := m.Params[len(m.Params)-1]
if len(args) > 0 {
buf.WriteByte(' ')
buf.WriteString(strings.Join(args, " "))
}
// If trailing is zero-length, contains a space or starts with
// a : we need to actually specify that it's trailing.
if len(trailing) == 0 || strings.ContainsRune(trailing, ' ') || trailing[0] == ':' {
buf.WriteString(" :")
} else {
buf.WriteString(" ")
}
buf.WriteString(trailing)
}
return buf.String()
}