Read lines with a bufio.Scanner, reuse input buffer, ignore empty messages, handle multiple spaces between tags and prefix

This commit is contained in:
Ken-Håvard Lieng 2019-01-11 04:53:50 +01:00
parent a3618b97ae
commit eee260f154
5 changed files with 80 additions and 50 deletions

View File

@ -30,7 +30,8 @@ type Client struct {
connected bool connected bool
registered bool registered bool
dialer *net.Dialer dialer *net.Dialer
reader *bufio.Reader recvBuf []byte
scan *bufio.Scanner
backoff *backoff.Backoff backoff *backoff.Backoff
out chan string out chan string
@ -51,6 +52,7 @@ func NewClient(nick, username string) *Client {
out: make(chan string, 32), out: make(chan string, 32),
quit: make(chan struct{}), quit: make(chan struct{}),
reconnect: make(chan struct{}), reconnect: make(chan struct{}),
recvBuf: make([]byte, 0, 4096),
backoff: &backoff.Backoff{ backoff: &backoff.Backoff{
Jitter: true, Jitter: true,
}, },

View File

@ -2,6 +2,7 @@ package irc
import ( import (
"bufio" "bufio"
"bytes"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"errors" "errors"
@ -151,7 +152,8 @@ func (c *Client) connect() error {
c.connected = true c.connected = true
c.connChange(true, nil) c.connChange(true, nil)
c.reader = bufio.NewReader(c.conn) c.scan = bufio.NewScanner(c.conn)
c.scan.Buffer(c.recvBuf, cap(c.recvBuf))
c.register() c.register()
@ -185,8 +187,7 @@ func (c *Client) recv() {
defer c.sendRecv.Done() defer c.sendRecv.Done()
for { for {
line, err := c.reader.ReadString('\n') if !c.scan.Scan() {
if err != nil {
select { select {
case <-c.quit: case <-c.quit:
return return
@ -203,7 +204,12 @@ func (c *Client) recv() {
} }
} }
msg := parseMessage(line) b := bytes.Trim(c.scan.Bytes(), " ")
if len(b) == 0 {
continue
}
msg := ParseMessage(string(b))
if msg == nil { if msg == nil {
close(c.quit) close(c.quit)
c.connChange(false, ErrBadProtocol) c.connChange(false, ErrBadProtocol)

View File

@ -129,19 +129,21 @@ func TestRecv(t *testing.T) {
buf.WriteString("CMD\r\n") buf.WriteString("CMD\r\n")
buf.WriteString("PING :test\r\n") buf.WriteString("PING :test\r\n")
buf.WriteString("001 foo\r\n") buf.WriteString("001 foo\r\n")
c.reader = bufio.NewReader(buf) c.scan = bufio.NewScanner(buf)
c.sendRecv.Add(1) c.sendRecv.Add(1)
go c.recv() go c.recv()
assert.Equal(t, "PONG :test\r\n", <-conn.hook) assert.Equal(t, "PONG :test\r\n", <-conn.hook)
assert.Equal(t, &Message{Command: "CMD"}, <-c.Messages) assert.Equal(t, &Message{Command: "CMD"}, <-c.Messages)
assert.Equal(t, &Message{Command: Ping, Params: []string{"test"}}, <-c.Messages)
assert.Equal(t, &Message{Command: ReplyWelcome, Params: []string{"foo"}}, <-c.Messages)
} }
func TestRecvTriggersReconnect(t *testing.T) { func TestRecvTriggersReconnect(t *testing.T) {
c := testClient() c := testClient()
c.conn = &mockConn{} c.conn = &mockConn{}
c.reader = bufio.NewReader(bytes.NewBufferString("001 bob\r\n")) c.scan = bufio.NewScanner(bytes.NewBufferString("001 bob\r\n"))
done := make(chan struct{}) done := make(chan struct{})
ok := false ok := false
go func() { go func() {

View File

@ -22,8 +22,7 @@ func (m *Message) LastParam() string {
return "" return ""
} }
func parseMessage(line string) *Message { func ParseMessage(line string) *Message {
line = strings.Trim(line, "\r\n ")
msg := Message{} msg := Message{}
if strings.HasPrefix(line, "@") { if strings.HasPrefix(line, "@") {
@ -35,7 +34,6 @@ func parseMessage(line string) *Message {
if len(tags) > 0 { if len(tags) > 0 {
msg.Tags = map[string]string{} msg.Tags = map[string]string{}
}
for _, tag := range tags { for _, tag := range tags {
key, val := splitParam(tag) key, val := splitParam(tag)
@ -49,7 +47,11 @@ func parseMessage(line string) *Message {
msg.Tags[key] = "" msg.Tags[key] = ""
} }
} }
}
for line[next+1] == ' ' {
next++
}
line = line[next+1:] line = line[next+1:]
} }
@ -73,7 +75,7 @@ func parseMessage(line string) *Message {
cmdEnd := len(line) cmdEnd := len(line)
trailing := "" trailing := ""
if i := strings.Index(line, " :"); i > 0 { if i := strings.Index(line, " :"); i >= 0 {
cmdEnd = i cmdEnd = i
trailing = line[i+2:] trailing = line[i+2:]
} }

View File

@ -12,7 +12,7 @@ func TestParseMessage(t *testing.T) {
expected *Message expected *Message
}{ }{
{ {
":user CMD #chan :some message\r\n", ":user CMD #chan :some message",
&Message{ &Message{
Prefix: "user", Prefix: "user",
Nick: "user", Nick: "user",
@ -20,7 +20,7 @@ func TestParseMessage(t *testing.T) {
Params: []string{"#chan", "some message"}, Params: []string{"#chan", "some message"},
}, },
}, { }, {
":nick!user@host.com CMD a b\r\n", ":nick!user@host.com CMD a b",
&Message{ &Message{
Prefix: "nick!user@host.com", Prefix: "nick!user@host.com",
Nick: "nick", Nick: "nick",
@ -28,80 +28,80 @@ func TestParseMessage(t *testing.T) {
Params: []string{"a", "b"}, Params: []string{"a", "b"},
}, },
}, { }, {
"CMD a b :\r\n", "CMD a b :",
&Message{ &Message{
Command: "CMD", Command: "CMD",
Params: []string{"a", "b", ""}, Params: []string{"a", "b", ""},
}, },
}, { }, {
"CMD a b\r\n", "CMD a b",
&Message{ &Message{
Command: "CMD", Command: "CMD",
Params: []string{"a", "b"}, Params: []string{"a", "b"},
}, },
}, { }, {
"CMD\r\n", "CMD",
&Message{ &Message{
Command: "CMD", Command: "CMD",
}, },
}, { }, {
"CMD :tests and stuff\r\n", "CMD :tests and stuff",
&Message{ &Message{
Command: "CMD", Command: "CMD",
Params: []string{"tests and stuff"}, Params: []string{"tests and stuff"},
}, },
}, { }, {
":nick@host.com CMD\r\n", ":nick@host.com CMD",
&Message{ &Message{
Prefix: "nick@host.com", Prefix: "nick@host.com",
Nick: "nick", Nick: "nick",
Command: "CMD", Command: "CMD",
}, },
}, { }, {
":ni@ck!user!name@host!.com CMD\r\n", ":ni@ck!user!name@host!.com CMD",
&Message{ &Message{
Prefix: "ni@ck!user!name@host!.com", Prefix: "ni@ck!user!name@host!.com",
Nick: "ni@ck", Nick: "ni@ck",
Command: "CMD", Command: "CMD",
}, },
}, { }, {
"CMD #cake pie \r\n", "CMD #cake pie ",
&Message{ &Message{
Command: "CMD", Command: "CMD",
Params: []string{"#cake", "pie"}, Params: []string{"#cake", "pie"},
}, },
}, { }, {
" CMD #cake pie\r\n", " CMD #cake pie",
&Message{ &Message{
Command: "CMD", Command: "CMD",
Params: []string{"#cake", "pie"}, Params: []string{"#cake", "pie"},
}, },
}, { }, {
"CMD #cake ::pie\r\n", "CMD #cake ::pie",
&Message{ &Message{
Command: "CMD", Command: "CMD",
Params: []string{"#cake", ":pie"}, Params: []string{"#cake", ":pie"},
}, },
}, { }, {
"CMD #cake : pie\r\n", "CMD #cake : pie",
&Message{ &Message{
Command: "CMD", Command: "CMD",
Params: []string{"#cake", " pie"}, Params: []string{"#cake", " pie"},
}, },
}, { }, {
"CMD #cake :pie :P <3\r\n", "CMD #cake :pie :P <3",
&Message{ &Message{
Command: "CMD", Command: "CMD",
Params: []string{"#cake", "pie :P <3"}, Params: []string{"#cake", "pie :P <3"},
}, },
}, { }, {
"CMD #cake :pie!\r\n", "CMD #cake :pie!",
&Message{ &Message{
Command: "CMD", Command: "CMD",
Params: []string{"#cake", "pie!"}, Params: []string{"#cake", "pie!"},
}, },
}, { }, {
"@x=y CMD\r\n", "@x=y CMD",
&Message{ &Message{
Tags: map[string]string{ Tags: map[string]string{
"x": "y", "x": "y",
@ -109,7 +109,7 @@ func TestParseMessage(t *testing.T) {
Command: "CMD", Command: "CMD",
}, },
}, { }, {
"@x=y :nick!user@host.com CMD\r\n", "@x=y :nick!user@host.com CMD",
&Message{ &Message{
Tags: map[string]string{ Tags: map[string]string{
"x": "y", "x": "y",
@ -119,7 +119,7 @@ func TestParseMessage(t *testing.T) {
Command: "CMD", Command: "CMD",
}, },
}, { }, {
"@x=y :nick!user@host.com CMD :pie and cake\r\n", "@x=y :nick!user@host.com CMD :pie and cake",
&Message{ &Message{
Tags: map[string]string{ Tags: map[string]string{
"x": "y", "x": "y",
@ -130,7 +130,19 @@ func TestParseMessage(t *testing.T) {
Params: []string{"pie and cake"}, Params: []string{"pie and cake"},
}, },
}, { }, {
"@x=y;a=b CMD\r\n", "@x=y :nick!user@host.com CMD beans rainbows :pie and cake",
&Message{
Tags: map[string]string{
"x": "y",
},
Prefix: "nick!user@host.com",
Nick: "nick",
Command: "CMD",
Params: []string{"beans", "rainbows", "pie and cake"},
},
},
{
"@x=y;a=b CMD",
&Message{ &Message{
Tags: map[string]string{ Tags: map[string]string{
"x": "y", "x": "y",
@ -139,7 +151,7 @@ func TestParseMessage(t *testing.T) {
Command: "CMD", Command: "CMD",
}, },
}, { }, {
"@x=y;a=\\\\\\:\\s\\r\\n CMD\r\n", "@x=y;a=\\\\\\:\\s\\r\\n CMD",
&Message{ &Message{
Tags: map[string]string{ Tags: map[string]string{
"x": "y", "x": "y",
@ -151,23 +163,29 @@ func TestParseMessage(t *testing.T) {
} }
for _, tc := range cases { for _, tc := range cases {
assert.Equal(t, tc.expected, parseMessage(tc.input)) assert.Equal(t, tc.expected, ParseMessage(tc.input))
}
}
func BenchmarkParseMessage(b *testing.B) {
for i := 0; i < b.N; i++ {
ParseMessage("@x=y :nick!user@host.com CMD beans rainbows :pie and cake")
} }
} }
func TestLastParam(t *testing.T) { func TestLastParam(t *testing.T) {
assert.Equal(t, "some message", parseMessage(":user CMD #chan :some message\r\n").LastParam()) assert.Equal(t, "some message", ParseMessage(":user CMD #chan :some message").LastParam())
assert.Equal(t, "", parseMessage("NO_PARAMS").LastParam()) assert.Equal(t, "", ParseMessage("NO_PARAMS").LastParam())
} }
func TestBadMessagePanic(t *testing.T) { func TestBadMessage(t *testing.T) {
parseMessage("@\r\n") assert.Nil(t, ParseMessage("@"))
parseMessage("@ :\r\n") assert.Nil(t, ParseMessage("@ :"))
parseMessage("@ :\r\n") assert.Nil(t, ParseMessage("@ :"))
parseMessage(":user\r\n") assert.Nil(t, ParseMessage("@ :"))
parseMessage(":\r\n") assert.Nil(t, ParseMessage(":user"))
parseMessage(":") assert.Nil(t, ParseMessage(":"))
parseMessage("") assert.Nil(t, ParseMessage(""))
} }
func TestParseISupport(t *testing.T) { func TestParseISupport(t *testing.T) {