// 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) }