package proto import ( "encoding/binary" "fmt" "io" ) // Header is the decoded form of a 16-byte CA message header. // All integer fields are stored in host byte order after decoding. type Header struct { Command uint16 PayloadSize uint32 // actual payload size (resolved from extended form if needed) DataType uint16 DataCount uint32 // actual element count (resolved from extended form if needed) Parameter1 uint32 Parameter2 uint32 } // HeaderSize is the wire size of a standard CA header. const HeaderSize = 16 // Encode writes the header to buf (must be at least HeaderSize bytes). // If PayloadSize > 0xFFFE or DataCount > 0xFFFF the extended encoding is used // and the caller must prepend an extra 8 bytes; use EncodeExtended instead. func (h Header) Encode(buf []byte) { binary.BigEndian.PutUint16(buf[0:], h.Command) binary.BigEndian.PutUint16(buf[2:], uint16(h.PayloadSize)) binary.BigEndian.PutUint16(buf[4:], h.DataType) binary.BigEndian.PutUint16(buf[6:], uint16(h.DataCount)) binary.BigEndian.PutUint32(buf[8:], h.Parameter1) binary.BigEndian.PutUint32(buf[12:], h.Parameter2) } // Bytes returns the 16-byte wire encoding of h. func (h Header) Bytes() []byte { buf := make([]byte, HeaderSize) h.Encode(buf) return buf } // DecodeHeader reads exactly one CA header from r, handling the extended // encoding (payload_size == 0xFFFF) transparently. // Returns the decoded Header and the number of bytes read (16 or 24). func DecodeHeader(r io.Reader) (Header, int, error) { var raw [HeaderSize]byte if _, err := io.ReadFull(r, raw[:]); err != nil { return Header{}, 0, fmt.Errorf("ca: read header: %w", err) } h := Header{ Command: binary.BigEndian.Uint16(raw[0:]), DataType: binary.BigEndian.Uint16(raw[4:]), Parameter1: binary.BigEndian.Uint32(raw[8:]), Parameter2: binary.BigEndian.Uint32(raw[12:]), } rawPayload := binary.BigEndian.Uint16(raw[2:]) rawCount := binary.BigEndian.Uint16(raw[6:]) // Extended message: payload_size == 0xFFFF means real sizes are in the // next 8 bytes (parameter1 = real payload size, parameter2 = real count). if rawPayload == 0xFFFF && rawCount == 0x0000 { var ext [8]byte if _, err := io.ReadFull(r, ext[:]); err != nil { return Header{}, 16, fmt.Errorf("ca: read extended header: %w", err) } h.PayloadSize = binary.BigEndian.Uint32(ext[0:]) h.DataCount = binary.BigEndian.Uint32(ext[4:]) // Note: parameter1/2 in the base header are overwritten by the extended values. // In practice, the original parameter1/2 are still valid — the extended header // only carries sizes, not the original parameters. We already decoded parameter1/2 // above from the base header. return h, 24, nil } h.PayloadSize = uint32(rawPayload) h.DataCount = uint32(rawCount) return h, 16, nil } // PadTo8 returns n rounded up to the nearest multiple of 8. // CA requires all message payloads to be padded to 8-byte boundaries. func PadTo8(n int) int { return (n + 7) &^ 7 } // PadBytes appends zero bytes to b until len(b) is a multiple of 8. func PadBytes(b []byte) []byte { need := PadTo8(len(b)) - len(b) return append(b, make([]byte, need)...) } // BuildMessage assembles a complete CA message (header + payload). // payload may be nil for zero-length messages. func BuildMessage(h Header, payload []byte) []byte { h.PayloadSize = uint32(len(payload)) msg := make([]byte, HeaderSize+len(payload)) h.Encode(msg) copy(msg[HeaderSize:], payload) return msg } // BuildStringPayload encodes a string as a null-terminated, 8-byte padded payload. func BuildStringPayload(s string) []byte { b := append([]byte(s), 0) // null terminator return PadBytes(b) }