first commit - still working on tests for edge cases

This commit is contained in:
William Dillon 2026-02-21 15:58:10 -05:00
commit f20fcb466d
4 changed files with 272 additions and 0 deletions

2
README.md Normal file
View File

@ -0,0 +1,2 @@
# msmsg
msmsg (Mad Scienstist Messaging Protocol) is a simple protocol for multiplexing, as specified by Pete McNeil. See https://www.lifeatwarp9.com/2016/02/channels-a-friendly-protocol-for-multiplexing-ipc/ for the full specs.

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module code.wmdillon.com/wmdillon/msmsg
go 1.25.5

207
message.go Normal file
View File

@ -0,0 +1,207 @@
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
}

60
message_test.go Normal file
View File

@ -0,0 +1,60 @@
package msmsg
import (
"reflect"
"testing"
)
var TheTestObjects = map[string]Message{
"<dialect ch= '0'>This message is opening this channel.</dialect>": {
Dialect: "dialect",
ID: "0",
Body: "This message is opening this channel.",
},
"<dialect ch ='0'></dialect>": {
Dialect: "dialect",
ID: "0",
},
"<dialect ch='0'/>": {
Dialect: "dialect",
ID: "0",
ClosesChannel: true,
},
"<dialect ch = '0' ctrl='shutdown' />": {
Dialect: "dialect",
ID: "0",
ClosesChannel: true,
Parameters: map[string]string{
"ctrl": "shutdown",
},
},
"<dialect>Hello</dialect>": {Dialect: "dialect", Body: "Hello"},
"this is chatter": {
Body: "this is chatter",
},
// these are malformed
"<dialect ch=10 />": {Body: "<dialect ch=10 />"},
"<dia": {Body: "<dia"},
"<dialect>Hello": {Body: "<dialect>Hello"},
"<dialect>Hello<dialect>": {Body: "<dialect>Hello<dialect>"},
"<dialect>Hello<dict>": {Body: "<dialect>Hello<dict>"},
"<dialect>Hello<dial ect>": {Body: "<dialect>Hello<dial ect>"},
}
func TestParseMessage(t *testing.T) {
for input, wantOutput := range TheTestObjects {
gotOutput := ParseMessage(input)
if want, got := wantOutput.Dialect, gotOutput.Dialect; want != got {
t.Fatalf("error parsing %s: wanted dialect %q; got %q\n", input, want, got)
} else if want, got := wantOutput.ID, gotOutput.ID; want != got {
t.Fatalf("error parsing %s: wanted ID %q; got %q\n", input, want, got)
} else if want, got := wantOutput.Body, gotOutput.Body; want != got {
t.Fatalf("error parsing %s: wanted body %q; got %q\n", input, want, got)
} else if !reflect.DeepEqual(wantOutput.Parameters, gotOutput.Parameters) {
t.Fatalf("error parsing %s: wanted parameters %+v; got %+v\n", input, wantOutput.Parameters, gotOutput.Parameters)
} else if want, got := wantOutput.ClosesChannel, gotOutput.ClosesChannel; want != got {
t.Fatalf("error parsing %s: wanted ClosesChannel=%v; got %v\n", input, want, got)
}
}
}