From 02ecc3928d3f475b4a4ad93d61c3da5b7b56d13b Mon Sep 17 00:00:00 2001 From: Kaleb Elwert Date: Wed, 17 Aug 2016 15:37:57 -0700 Subject: [PATCH 1/2] Convert parser testing to testify --- parser_test.go | 43 +++++++++++++------------------------------ 1 file changed, 13 insertions(+), 30 deletions(-) diff --git a/parser_test.go b/parser_test.go index 26951b6..7e68ea2 100644 --- a/parser_test.go +++ b/parser_test.go @@ -1,7 +1,6 @@ package irc import ( - "reflect" "testing" "github.com/stretchr/testify/assert" @@ -170,15 +169,10 @@ func TestParsePrefix(t *testing.T) { t.Errorf("%d. Got nil for valid identity", i) continue } - if test.Name != pi.Name { - t.Errorf("%d. name = %q, want %q", i, pi.Name, test.Name) - } - if test.User != pi.User { - t.Errorf("%d. user = %q, want %q", i, pi.User, test.User) - } - if test.Host != pi.Host { - t.Errorf("%d. host = %q, want %q", i, pi.Host, test.Host) - } + + assert.Equal(t, test.Name, pi.Name, "%d. Wrong Name", i) + assert.Equal(t, test.User, pi.User, "%d. Wrong User", i) + assert.Equal(t, test.Host, pi.Host, "%d. Wrong Host", i) } } @@ -199,11 +193,9 @@ func TestMessageTrailing(t *testing.T) { m := ParseMessage(test.Expect) tr := m.Trailing() if len(test.Params) < 1 { - if tr != "" { - t.Errorf("%d. trailing = %q, want %q", i, tr, "") - } - } else if tr != test.Params[len(test.Params)-1] { - t.Errorf("%d. trailing = %q, want %q", i, tr, test.Params[len(test.Params)-1]) + assert.Equal(t, "", tr, "%d. Expected empty trailing", i) + } else { + assert.Equal(t, test.Params[len(test.Params)-1], tr, "%d. Expected matching traling", i) } } } @@ -217,9 +209,7 @@ func TestMessageFromChan(t *testing.T) { } m := ParseMessage(test.Expect) - if m.FromChannel() != test.FromChan { - t.Errorf("%d. fromchannel = %v, want %v", i, m.FromChannel(), test.FromChan) - } + assert.Equal(t, test.FromChan, m.FromChannel(), "%d. Wrong FromChannel value", i) } } @@ -234,21 +224,16 @@ func TestMessageCopy(t *testing.T) { m := ParseMessage(test.Expect) c := m.Copy() - if !reflect.DeepEqual(m, c) { - t.Errorf("%d. copy = %q, want %q", i, m, c) - } + assert.EqualValues(t, m, c, "%d. Copied values are not equal", i) if c.Prefix != nil { c.Prefix.Name += "junk" - if reflect.DeepEqual(m, c) { - t.Errorf("%d. copyidentity matched when it shouldn't", i) - } + + assert.False(t, assert.ObjectsAreEqualValues(m, c), "%d. Copied with modified identity should not match", i) } c.Params = append(c.Params, "junk") - if reflect.DeepEqual(m, c) { - t.Errorf("%d. copyargs matched when it shouldn't", i) - } + assert.False(t, assert.ObjectsAreEqualValues(m, c), "%d. Copied with additional params should not match", i) } } @@ -261,8 +246,6 @@ func TestMessageString(t *testing.T) { } m := ParseMessage(test.Expect) - if m.String()+"\n" != test.Expect { - t.Errorf("%d. %s did not match %s", i, m.String(), test.Expect) - } + assert.Equal(t, test.Expect, m.String()+"\n", "%d. Message Stringification failed", i) } } From 7fe84c6a834f768e75ee8347674cda73a9b02ec8 Mon Sep 17 00:00:00 2001 From: Kaleb Elwert Date: Wed, 17 Aug 2016 17:08:27 -0700 Subject: [PATCH 2/2] Add support for encoding and decoding IRCv3 message tags --- .gitignore | 1 + client_test.go | 1 + parser.go | 166 +++++++++++++++++++++++++++++++++++++++++++++---- parser_test.go | 123 +++++++++++++++++++++++++++++++++++- 4 files changed, 275 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 31318c9..b64fa18 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.cover *.test +*.out diff --git a/client_test.go b/client_test.go index 7b283e4..6501c8f 100644 --- a/client_test.go +++ b/client_test.go @@ -110,6 +110,7 @@ func TestClientHandler(t *testing.T) { assert.EqualValues(t, []*Message{ &Message{ + Tags: Tags{}, Prefix: &Prefix{}, Command: "001", Params: []string{"hello_world"}, diff --git a/parser.go b/parser.go index ebd013a..84d8fac 100644 --- a/parser.go +++ b/parser.go @@ -5,6 +5,123 @@ import ( "strings" ) +var tagDecodeSlashMap = map[rune]rune{ + ':': ';', + 's': ' ', + '\\': '\\', + 'r': '\r', + 'n': '\n', +} + +var tagEncodeMap = map[rune]string{ + ';': "\\:", + ' ': "\\s", + '\\': "\\\\", + '\r': "\\r", + '\n': "\\n", +} + +// TagValue represents the value of a tag. +type TagValue string + +// ParseTagValue parses a TagValue from the connection. If you need to +// set a TagValue, you probably want to just set the string itself, so +// it will be encoded properly. +func ParseTagValue(v string) TagValue { + ret := &bytes.Buffer{} + + input := bytes.NewBufferString(v) + + for { + c, _, err := input.ReadRune() + if err != nil { + break + } + + if c == '\\' { + c2, _, err := input.ReadRune() + if err != nil { + ret.WriteRune(c) + break + } + + if replacement, ok := tagDecodeSlashMap[c2]; ok { + ret.WriteRune(replacement) + } else { + ret.WriteRune(c) + ret.WriteRune(c2) + } + } else { + ret.WriteRune(c) + } + } + + return TagValue(ret.String()) +} + +// Encode converts a TagValue to the format in the connection. +func (v TagValue) Encode() 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]TagValue + +// 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 +} + +// GetTag is a convenience method to look up a tag in the map. +func (t Tags) GetTag(key string) (string, bool) { + ret, ok := t[key] + return string(ret), ok +} + +// 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(v.Encode()) + } + } + + // 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 @@ -18,18 +135,6 @@ type Prefix struct { Host string } -// Message represents a line parsed from the server -type Message struct { - // 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 -} - // ParsePrefix takes an identity string and parses it into an // identity struct. It will always return an Prefix struct and never // nil. @@ -79,6 +184,21 @@ func (p *Prefix) String() string { 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 +} + // 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. @@ -89,7 +209,20 @@ func ParseMessage(line string) *Message { return nil } - c := &Message{Prefix: &Prefix{}} + c := &Message{ + Tags: Tags{}, + Prefix: &Prefix{}, + } + + if line[0] == '@' { + split := strings.SplitN(line, " ", 2) + if len(split) < 2 { + return nil + } + + c.Tags = ParseTags(split[0][1:]) + line = split[1] + } if line[0] == ':' { split := strings.SplitN(line, " ", 2) @@ -176,6 +309,13 @@ func (m *Message) Copy() *Message { 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.Name != "" { buf.WriteByte(':') diff --git a/parser_test.go b/parser_test.go index 7e68ea2..ae1c849 100644 --- a/parser_test.go +++ b/parser_test.go @@ -11,12 +11,16 @@ var messageTests = []struct { Prefix, Cmd string Params []string + // Tag parsing + Tags Tags + // Prefix parsing Name, User, Host string // Total output - Expect string - IsNil bool + Expect string + ExpectIn []string + IsNil bool // FromChannel FromChan bool @@ -32,6 +36,10 @@ var messageTests = []struct { Expect: ":A", IsNil: true, }, + { + Expect: "@A", + IsNil: true, + }, { Prefix: "server.kevlar.net", Cmd: "PING", @@ -127,6 +135,89 @@ var messageTests = []struct { Expect: ":A B C D\n", }, + { + Tags: Tags{ + "tag": "value", + }, + + Params: []string{}, + Cmd: "A", + + Expect: "@tag=value A\n", + }, + { + Tags: Tags{ + "tag": "\n", + }, + + Params: []string{}, + Cmd: "A", + + Expect: "@tag=\\n A\n", + }, + { + Tags: Tags{ + "tag": "\\", + }, + + Params: []string{}, + Cmd: "A", + + Expect: "@tag=\\ A\n", + ExpectIn: []string{"@tag=\\\\ A\n"}, + }, + { + Tags: Tags{ + "tag": ";", + }, + + Params: []string{}, + Cmd: "A", + + Expect: "@tag=\\: A\n", + }, + { + Tags: Tags{ + "tag": "", + }, + + Params: []string{}, + Cmd: "A", + + Expect: "@tag A\n", + }, + { + Tags: Tags{ + "tag": "\\&", + }, + + Params: []string{}, + Cmd: "A", + + Expect: "@tag=\\& A\n", + ExpectIn: []string{"@tag=\\\\& A\n"}, + }, + { + Tags: Tags{ + "tag": "x", + "tag2": "asd", + }, + + Params: []string{}, + Cmd: "A", + + Expect: "@tag=x;tag2=asd A\n", + ExpectIn: []string{"@tag=x;tag2=asd A\n", "@tag2=asd;tag=x A\n"}, + }, + { + Tags: Tags{ + "tag": "; \\\r\n", + }, + + Params: []string{}, + Cmd: "A", + Expect: "@tag=\\:\\s\\\\\\r\\n A\n", + }, } func TestParseMessage(t *testing.T) { @@ -246,6 +337,32 @@ func TestMessageString(t *testing.T) { } m := ParseMessage(test.Expect) - assert.Equal(t, test.Expect, m.String()+"\n", "%d. Message Stringification failed", i) + if test.ExpectIn != nil { + assert.Contains(t, test.ExpectIn, m.String()+"\n", "%d. Message Stringification failed", i) + } else { + assert.Equal(t, test.Expect, m.String()+"\n", "%d. Message Stringification failed", i) + } + } +} + +func TestMessageTags(t *testing.T) { + t.Parallel() + + for i, test := range messageTests { + if test.IsNil || test.Tags == nil { + continue + } + + m := ParseMessage(test.Expect) + assert.EqualValues(t, test.Tags, m.Tags, "%d. Tag parsing failed", i) + + // Ensure we have all the tags we expected. + for k, v := range test.Tags { + tag, ok := m.GetTag(k) + assert.True(t, ok, "%d. Missing tag %q", i, k) + assert.EqualValues(t, v, tag, "%d. Wrong tag value", i) + } + + assert.EqualValues(t, test.Tags, m.Tags, "%d. Tags don't match", i) } }