212 lines
4.8 KiB
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)
|
|
}
|