Read lines with a bufio.Scanner, reuse input buffer, ignore empty messages, handle multiple spaces between tags and prefix
This commit is contained in:
parent
a3618b97ae
commit
eee260f154
@ -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,
|
||||||
},
|
},
|
||||||
|
@ -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)
|
||||||
|
@ -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() {
|
||||||
|
@ -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:]
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
Loading…
Reference in New Issue
Block a user