Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
*.cover
*.test
*.out
1 change: 1 addition & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ func TestClientHandler(t *testing.T) {

assert.EqualValues(t, []*Message{
&Message{
Tags: Tags{},
Prefix: &Prefix{},
Command: "001",
Params: []string{"hello_world"},
Expand Down
166 changes: 153 additions & 13 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,123 @@ import (
"strings"
)

var tagDecodeSlashMap = map[rune]rune{
':': ';',
's': ' ',
'\\': '\\',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow this, why switch from the same character to itself?

Copy link
Member Author

@belak belak Aug 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is so \\ will decode to \ (tagDecodeSlashMap refers to the character that comes after a slash).

'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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's no way to automatically unescape values in golang? this seems like it's been written before

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't been able to find one, but I'd be happy to change it if we find one later.

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(';')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why Byte here and Rune above?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Byte is slightly faster as a rune can contain multiple bytes.

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
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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)
Expand Down Expand Up @@ -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(':')
Expand Down
Loading