msmsg/message.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
}