diff --git a/message.go b/message.go index fb6fb18..4349277 100644 --- a/message.go +++ b/message.go @@ -2,7 +2,6 @@ package main import ( "bytes" - "fmt" "strings" ) @@ -11,12 +10,13 @@ type Tag struct { Value string } -// Tags is an key-order-preserving, last-insert-wins "set" of Tag +// Tags is an key-order-preserving, last-insert-wins "set" of Tag{}s type Tags struct { kv map[string]int // quick lookups for inserts tags []Tag } +// Insert adds the given tag to the set, overriding any existing tag with that Key func (t *Tags) Insert(new Tag) { if t.kv == nil { t.kv = map[string]int{} @@ -30,38 +30,54 @@ func (t *Tags) Insert(new Tag) { } } +// GetSlice returns a simple slice of tags from the set func (t *Tags) GetSlice() []Tag { return t.tags } +// A Message represents a parsed set of Tags/Source/Command/Parameters from a Raw string. +// +// Tags/Source/Command/Parameters are all set from the Raw after ParseMessage() is called. +// Calling ParseMessage multiple times (without changing Raw) should be a no-op. +// +// Canonicalize() returns Tags/Source/Command/Parameters transformed back into an RFC-compliant []byte. +// +// Reusing a message struct to parse multiple Raw messages is possible, but frowned upon. +// +// Tag values are automatically escaped during parsing. type Message struct { - Tags []Tag - Source string - Command command - Parameters []string + Tags []Tag // An ordered slice of Key=Value pairs + Source string // An optional string denoting the source of the message + Command command // A command/numeric for the message + Parameters []string // Zero or more parameters for the Command. Raw string parseIndex int } -func (m *Message) ParseMessage() error { +// ParseMessage will attempt to parse whatever it's given as an IRC Message, +// and populate Tags/Source/Command/Parameters with the results. +// Garbage in, garbage out, though. The parser tries its best, but doesn't aggressively +// validate messages. +// +// Tag values are automatically escaped during parsing, as needed. +func (m *Message) ParseMessage() { m.parseIndex = 0 m.consumeSpaces() if m.consume('@') { - m.Tags, _ = m.parseTags() + m.Tags = m.parseTags() } m.consumeSpaces() if m.consume(':') { - m.Source, _ = m.parseSource() + m.Source = m.parseSource() } m.consumeSpaces() - m.Command, _ = m.parseCommand() + m.Command = m.parseCommand() m.consumeSpaces() - m.Parameters, _ = m.parseParameters() - return nil + m.Parameters = m.parseParameters() } -func (m *Message) parseTags() ([]Tag, error) { +func (m *Message) parseTags() []Tag { tags := &Tags{} for { next := m.findNext("=", ";") @@ -71,30 +87,34 @@ func (m *Message) parseTags() ([]Tag, error) { key := m.Raw[m.parseIndex : m.parseIndex+next] m.parseIndex += next + 1 eot := m.findNext(";", " ") + if eot == -1 { // ran out of message, probably + tags.Insert(Tag{Key: key, Value: unescapeTag(m.Raw[m.parseIndex:])}) + m.parseIndex = len(m.Raw) + break + } tags.Insert(Tag{Key: key, Value: unescapeTag(m.Raw[m.parseIndex : m.parseIndex+eot])}) m.parseIndex += len(m.Raw[m.parseIndex:m.parseIndex+eot]) + 1 } else if m.Raw[m.parseIndex+next] == ';' { key := m.Raw[m.parseIndex : m.parseIndex+next] m.parseIndex += next + 1 tags.Insert(Tag{Key: key, Value: ""}) - } else { - break } } - return tags.GetSlice(), nil + return tags.GetSlice() } -func (m *Message) parseSource() (string, error) { +func (m *Message) parseSource() string { start := m.parseIndex endofparse := strings.Index(m.Raw[m.parseIndex:], " ") if endofparse == -1 { - return "", fmt.Errorf("end of string encountered while parsing tags") + m.parseIndex = len(m.Raw) // out of message! which is weird. + } else { + m.parseIndex += endofparse } - m.parseIndex += endofparse - return m.Raw[start:m.parseIndex], nil + return m.Raw[start:m.parseIndex] } -func (m *Message) parseCommand() (command, error) { +func (m *Message) parseCommand() command { start := m.parseIndex endofparse := strings.Index(m.Raw[m.parseIndex:], " ") if endofparse == -1 { @@ -102,10 +122,10 @@ func (m *Message) parseCommand() (command, error) { } else { m.parseIndex += endofparse } - return command(m.Raw[start:m.parseIndex]), nil + return command(m.Raw[start:m.parseIndex]) } -func (m *Message) parseParameters() ([]string, error) { +func (m *Message) parseParameters() []string { params := []string{} for m.parseIndex <= len(m.Raw) { m.consumeSpaces() @@ -114,17 +134,13 @@ func (m *Message) parseParameters() ([]string, error) { break } endofparse := strings.Index(m.Raw[m.parseIndex:], " ") - if endofparse == -1 { // no further params after this one - p := strings.TrimSuffix(m.Raw[m.parseIndex:], " ") - if len(p) > 0 { // reject empty trailing params - params = append(params, p) - } + if endofparse == -1 { // no further params break } params = append(params, m.Raw[m.parseIndex:m.parseIndex+endofparse]) m.parseIndex += endofparse } - return params, nil + return params } func (m *Message) consumeSpaces() { @@ -148,9 +164,6 @@ func (m *Message) consume(r byte) bool { } func (m *Message) findNext(bs ...string) int { - if len(bs) == 0 { - return 0 - } r := -1 for _, b := range bs { i := strings.Index(m.Raw[m.parseIndex:], b) @@ -167,9 +180,6 @@ func unescapeTag(s string) string { i := 0 for i < len(s) { if escaping { - if i+1 > len(s) { - break - } switch s[i] { case ':': sb.WriteByte(';') diff --git a/message_test.go b/message_test.go index e99435c..a659ff4 100644 --- a/message_test.go +++ b/message_test.go @@ -136,6 +136,12 @@ func TestParsing(t *testing.T) { Tags: []Tag{{"tag1", `value`}}, Command: command("COMMAND"), }}, + { + input: `@tag1=va\r\nlue COMMAND`, + output: &Message{ + Tags: []Tag{{"tag1", "va\r\nlue"}}, + Command: command("COMMAND"), + }}, { input: `@tag1=1;tag2=3;tag3=4;tag1=5 COMMAND`, output: &Message{ @@ -162,6 +168,24 @@ func TestParsing(t *testing.T) { Command: MODE, Parameters: []string{"#channel", "+oo", "SomeUser", "AnotherUser"}, }}, + { + input: `:uttergarbage`, + output: &Message{ + Source: "uttergarbage", + }, + }, + { + input: `@only=tags`, + output: &Message{ + Tags: []Tag{{"only", "tags"}}, + }, + }, + { + input: `@only=`, + output: &Message{ + Tags: []Tag{{"only", ""}}, + }, + }, } for _, tc := range testcases { m := &Message{Raw: tc.input} @@ -305,6 +329,13 @@ func TestCanonicalization(t *testing.T) { }, output: `@tag1=value\\ntest COMMAND`, }, + { + input: &Message{ + Tags: []Tag{{"tag1", "va\r\nlue"}}, + Command: command("COMMAND"), + }, + output: `@tag1=va\r\nlue COMMAND`, + }, } for _, tc := range testcases { output := string(tc.input.Canonicalize()) @@ -314,3 +345,14 @@ func TestCanonicalization(t *testing.T) { } } } + +func TestRoundTrip(t *testing.T) { + in := `@a=b;c=3\n2;k;rt=ql7 :dan!d@localhost PRIVMSG #chan :Hey what's up! ` + m := &Message{Raw: in} + m.ParseMessage() + out := m.Canonicalize() + if string(out) != in { + t.Logf("expected '%s', received '%s'", in, string(out)) + t.Fail() + } +}