src.dualinventive.com/go/lib/cp3000/cp3000.go

212 lines
4.8 KiB
Go

// Package cp3000 provides functionality to talk to legacy servers using the deplicated cp3000 protocol
package cp3000
import (
"bytes"
"fmt"
"io"
"strconv"
)
// Type is the CP3000 messagetype
type Type byte
// state is the CP3000 decoder state
type state int
const (
stateStart state = iota
stateCmd
stateArgs
stateChecksum
stateEnd
)
const (
// TypeReply is a CP3000 reply
TypeReply Type = '!'
// TypeCommand is a CP3000 command (request or publish)
TypeCommand Type = '$'
)
// Msg represents a CP3000 message
type Msg struct {
Command Command
Params []string
Type Type
}
// Bytes return the CP3000 raw data
func (m Msg) Bytes() []byte {
// Build the buffer. The buffer contains the command and params
var buffer bytes.Buffer
_, err := fmt.Fprintf(&buffer, "%c%s", m.Type, m.Command)
if err != nil {
return nil
}
for _, param := range m.Params {
_, err = fmt.Fprintf(&buffer, ",%s", strconv.QuoteToASCII(param))
if err != nil {
return nil
}
}
// Write the checksum after the buffer
_, err = fmt.Fprintf(&buffer, "*%02X\n", calcChecksum(buffer))
if err != nil {
return nil
}
return buffer.Bytes()
}
func (m Msg) String() string {
return string(m.Bytes())
}
// Decode decodes CP3000 commands
//nolint: gocyclo
func Decode(line string) (*Msg, error) {
msg := &Msg{}
var arg bytes.Buffer
var quoted bool
var checksumBytes []byte
checksum := 0
state := stateStart
buffer := bytes.NewBufferString(line)
var err error
var b, prev byte
for err == nil {
b, err = buffer.ReadByte()
// except the start and the checksum everything is used for the checksum
if state != stateStart && state != stateChecksum {
// if we find an asterisk outside a quoted section it is the start-of-checksum
// this charater must be skipped!
if !(b == '*' && !quoted) {
checksum ^= int(b)
}
}
switch state {
case stateStart:
// a message always starts with $ or !
if b != byte(TypeReply) && b != byte(TypeCommand) {
return nil, fmt.Errorf("no cmd start: %c", b)
}
msg.Type = Type(b)
state = stateCmd
case stateCmd:
// a command has a variable length and ends on:
// - a comma (i.e. start of arguments)
// - an asterisk (i.e. begin of checksum)
if b == ',' {
state = stateArgs
} else if b == '*' {
state = stateChecksum
} else {
msg.Command += Command(b)
}
case stateArgs:
// Arguments are none or multiple chunks split by comma's and ended by the
// beginning of the checksum ('*'). Strings MAY be quoted "bla\"" and must be
// escaped.
if quoted || (b != ',' && b != '*') {
if b == '"' && prev != '\\' {
quoted = !quoted
}
erri := arg.WriteByte(b)
_ = erri
} else {
newArg, qerr := strconv.Unquote(arg.String())
if qerr != nil {
// it is not required to have quoted strings, allow strings where unquote fails
newArg = arg.String()
}
msg.Params = append(msg.Params, newArg)
// beginning of checksum
if b == '*' {
state = stateChecksum
}
arg.Reset()
}
prev = b
case stateChecksum:
// the checksum is 2 bytes which is the result of all XOR-ed bytes
checksumBytes = append(checksumBytes, b)
if len(checksumBytes) == 2 {
if fmt.Sprintf("%02X", checksum) != string(checksumBytes) {
return nil, fmt.Errorf("checksum mismatch (recv %s, calc %02X)", checksumBytes, checksum)
}
state = stateEnd
}
case stateEnd:
// nothing to do anymore
}
}
// When something else occurred than io.EOF, return that error
if err != nil && err != io.EOF {
return nil, err
}
if state != stateEnd {
return nil, fmt.Errorf("incomplete message")
}
return msg, nil
}
// SendReply sends a reply via CP3000
func SendReply(w io.Writer, reply uint8, params ...string) error {
cmd := Msg{
Type: TypeReply,
Command: replyToCommand(reply),
Params: params,
}
// Write to w
_, err := w.Write(cmd.Bytes())
return err
}
// Send will create a cp3000 command with the given parameters and writes to w.
// W can be a TCP connection (for direct communication) or any other communication.
func Send(w io.Writer, command Command, params ...string) error {
cmd := Msg{
Type: TypeCommand,
Command: command,
Params: params,
}
// Write to w
_, err := w.Write(cmd.Bytes())
return err
}
// calcChecksum will calculate the cp3000 checksum of the given buffer.
// The buffer may start with $ and end with * (start and end of the protocol)
func calcChecksum(buffer bytes.Buffer) uint8 {
var checksum int
if b, err := buffer.ReadByte(); err == nil {
if b != '$' && b != '!' {
checksum ^= int(b)
}
}
var b byte
var err error
quoted := false
for ; err == nil; b, err = buffer.ReadByte() {
if b == '"' {
quoted = !quoted
}
if b == '*' && !quoted {
break
}
checksum ^= int(b)
}
return uint8(checksum & 0xff)
}