208 lines
4.8 KiB
Go
208 lines
4.8 KiB
Go
package msmsg
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
type Message struct {
|
|
Dialect string
|
|
ID string
|
|
Parameters map[string]string
|
|
Body string
|
|
ClosesChannel bool
|
|
}
|
|
|
|
func (m Message) String() string {
|
|
switch {
|
|
case m.Chatter():
|
|
return m.Body
|
|
}
|
|
var builder strings.Builder
|
|
|
|
return builder.String()
|
|
}
|
|
|
|
func ByteIsSpace(b byte) bool {
|
|
return b == ' ' ||
|
|
b == '\t' ||
|
|
b == '\r' ||
|
|
b == '\n'
|
|
}
|
|
|
|
// Lines that are not encapsulated in XML tags or
|
|
// are not recognized as part of a known dialect
|
|
// are called chatter.
|
|
func (m *Message) Chatter() bool {
|
|
return len(m.Dialect) == 0 && len(m.ID) == 0 && len(m.Parameters) == 0
|
|
}
|
|
|
|
// Lines that have no associated ChannelID are
|
|
// called anonymous messages.
|
|
func (m *Message) Anonymous() bool {
|
|
return !m.Chatter() && len(m.ID) == 0
|
|
}
|
|
|
|
type token struct {
|
|
dialect string
|
|
id string
|
|
parameters map[string]string
|
|
body string
|
|
closesMessage bool
|
|
closesChannel bool
|
|
}
|
|
|
|
var (
|
|
errTokenMalformed = errors.New("token is malformed")
|
|
)
|
|
|
|
func parseToken(s string) (token, error) {
|
|
var results token
|
|
|
|
if !strings.HasPrefix(s, "<") || !strings.HasSuffix(s, ">") {
|
|
return token{body: s}, fmt.Errorf("%w: missing opening and/or closing angle bracket", errTokenMalformed)
|
|
}
|
|
|
|
buffer := strings.TrimSpace(s[1 : len(s)-1])
|
|
if strings.HasPrefix(buffer, "/") {
|
|
results.closesMessage = true
|
|
buffer = strings.TrimSpace(buffer[1:])
|
|
}
|
|
if strings.HasSuffix(buffer, "/") {
|
|
results.closesMessage = true
|
|
results.closesChannel = true
|
|
buffer = strings.TrimSpace(buffer[:len(buffer)-1])
|
|
}
|
|
|
|
// try to grab the first word off the front, this should be the dialect
|
|
i := strings.IndexAny(buffer, " \t\r\n")
|
|
if i == -1 {
|
|
results.dialect = buffer
|
|
return results, nil
|
|
} else if i < len(buffer) {
|
|
// is the next non-whitespace char an '='?
|
|
for j := i + 1; j < len(buffer); j++ {
|
|
if ByteIsSpace(buffer[j]) {
|
|
i = j
|
|
break
|
|
} else if buffer[j] == '=' {
|
|
i = 0
|
|
break
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
if i > 0 {
|
|
results.dialect = buffer[:i]
|
|
buffer = strings.TrimSpace(buffer[i:])
|
|
}
|
|
}
|
|
|
|
// everything else is presumed to be key=value pairs
|
|
type STATE int8
|
|
const (
|
|
KEY STATE = iota
|
|
VALUE
|
|
)
|
|
var key, value strings.Builder
|
|
var quote byte
|
|
var state STATE
|
|
for i = 0; i < len(buffer); i++ {
|
|
theChar := buffer[i]
|
|
switch state {
|
|
case KEY:
|
|
// parse the key and prepare to parse the value by finding the '=' and the quotation mark
|
|
if ByteIsSpace(theChar) {
|
|
continue
|
|
} else if theChar == '=' {
|
|
// find the next quotation mark
|
|
for i = i + 1; i < len(buffer); i++ {
|
|
theChar = buffer[i]
|
|
if ByteIsSpace(theChar) {
|
|
continue
|
|
} else if theChar == '\'' || theChar == '"' {
|
|
quote = theChar
|
|
state = VALUE
|
|
break
|
|
} else {
|
|
return token{body: s}, fmt.Errorf("%w: parameter value missing quotation mark", errTokenMalformed)
|
|
}
|
|
}
|
|
} else {
|
|
key.WriteByte(theChar)
|
|
}
|
|
case VALUE:
|
|
// if quote == \0 something bad has happened - we don't know what we're looking for
|
|
if quote == '\x00' {
|
|
return token{body: s}, fmt.Errorf("%w: missing quotation mark in VALUE state...", errTokenMalformed)
|
|
} else if theChar == quote {
|
|
// we have a key and a value - add them to the parameters
|
|
if results.parameters == nil {
|
|
results.parameters = make(map[string]string, 1)
|
|
}
|
|
results.parameters[key.String()] = value.String()
|
|
key.Reset()
|
|
value.Reset()
|
|
quote = '\x00'
|
|
state = KEY
|
|
continue
|
|
} else {
|
|
value.WriteByte(theChar)
|
|
}
|
|
}
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// <dialect ch='0'>This message is opening this channel.</dialect>
|
|
|
|
func ParseMessage(s string) Message {
|
|
var message Message
|
|
if !strings.HasPrefix(s, "<") || !strings.HasSuffix(s, ">") {
|
|
message.Body = s
|
|
}
|
|
headerEnd := strings.IndexByte(s, '>')
|
|
headerString := s[:headerEnd+1]
|
|
header, err := parseToken(headerString)
|
|
if err != nil {
|
|
return Message{Body: s}
|
|
}
|
|
message.Dialect = strings.Clone(header.dialect)
|
|
for k, v := range header.parameters {
|
|
switch strings.ToLower(k) {
|
|
case "ch":
|
|
message.ID = strings.Clone(v)
|
|
default:
|
|
if message.Parameters == nil {
|
|
message.Parameters = make(map[string]string, len(header.parameters))
|
|
}
|
|
message.Parameters[strings.Clone(k)] = strings.Clone(v)
|
|
}
|
|
}
|
|
// is there a footer?
|
|
if headerEnd == len(s)-1 {
|
|
if header.closesChannel {
|
|
message.ClosesChannel = true
|
|
}
|
|
return message
|
|
}
|
|
footerStart := strings.LastIndexByte(s, '<')
|
|
if footerStart <= 0 {
|
|
// malformed
|
|
return Message{Body: s}
|
|
}
|
|
footer, err := parseToken(s[footerStart:])
|
|
if err != nil {
|
|
return Message{Body: s}
|
|
}
|
|
if want, got := header.dialect, footer.dialect; want != got {
|
|
return Message{Body: s}
|
|
} else if !footer.closesMessage {
|
|
return Message{Body: s}
|
|
}
|
|
// the stuff between is the body
|
|
message.Body = strings.TrimSpace(s[headerEnd+1 : footerStart])
|
|
return message
|
|
}
|