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 } // This message is opening this channel. 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 }