// Package helpers provides shared utilities for WebSocket handlers. package helpers import ( "encoding/binary" "fmt" ) // PayloadParser provides helpers for parsing binary WebSocket payloads. type PayloadParser struct { payload []byte offset int } // NewPayloadParser creates a new payload parser starting after the API key hash. func NewPayloadParser(payload []byte, apiKeyHashLen int) *PayloadParser { return &PayloadParser{ payload: payload, offset: apiKeyHashLen, } } // ParseByte parses a single byte and advances the offset. func (p *PayloadParser) ParseByte() (byte, error) { if p.offset >= len(p.payload) { return 0, fmt.Errorf("payload too short at offset %d", p.offset) } b := p.payload[p.offset] p.offset++ return b, nil } // ParseUint16 parses a 2-byte big-endian uint16 and advances the offset. func (p *PayloadParser) ParseUint16() (uint16, error) { if p.offset+2 > len(p.payload) { return 0, fmt.Errorf("payload too short for uint16 at offset %d", p.offset) } v := binary.BigEndian.Uint16(p.payload[p.offset : p.offset+2]) p.offset += 2 return v, nil } // ParseLengthPrefixedString parses a length-prefixed string. // Format: [length:1][string:var] func (p *PayloadParser) ParseLengthPrefixedString() (string, error) { if p.offset >= len(p.payload) { return "", fmt.Errorf("payload too short for length at offset %d", p.offset) } length := int(p.payload[p.offset]) p.offset++ if length < 0 { return "", fmt.Errorf("invalid negative length at offset %d", p.offset-1) } if p.offset+length > len(p.payload) { return "", fmt.Errorf("payload too short for string of length %d at offset %d", length, p.offset) } str := string(p.payload[p.offset : p.offset+length]) p.offset += length return str, nil } // ParseUint16PrefixedString parses a string prefixed by a 2-byte length. // Format: [length:2][string:var] func (p *PayloadParser) ParseUint16PrefixedString() (string, error) { if p.offset+2 > len(p.payload) { return "", fmt.Errorf("payload too short for uint16 length at offset %d", p.offset) } length := int(binary.BigEndian.Uint16(p.payload[p.offset : p.offset+2])) p.offset += 2 if length < 0 { return "", fmt.Errorf("invalid negative length at offset %d", p.offset-2) } if p.offset+length > len(p.payload) { return "", fmt.Errorf("payload too short for string of length %d at offset %d", length, p.offset) } str := string(p.payload[p.offset : p.offset+length]) p.offset += length return str, nil } // Payload returns the underlying payload bytes. func (p *PayloadParser) Payload() []byte { return p.payload } // Offset returns the current offset into the payload. func (p *PayloadParser) Offset() int { return p.offset } // HasRemaining returns true if there are remaining bytes. func (p *PayloadParser) HasRemaining() bool { return p.offset < len(p.payload) } // Remaining returns the remaining bytes in the payload from current offset. func (p *PayloadParser) Remaining() []byte { if p.offset >= len(p.payload) { return nil } return p.payload[p.offset:] } // ParseBool parses a byte as a boolean (0 = false, non-zero = true). func (p *PayloadParser) ParseBool() (bool, error) { b, err := p.ParseByte() if err != nil { return false, err } return b != 0, nil } // ParseFixedBytes parses a fixed-length byte slice. func (p *PayloadParser) ParseFixedBytes(length int) ([]byte, error) { if p.offset+length > len(p.payload) { return nil, fmt.Errorf("payload too short for %d bytes at offset %d", length, p.offset) } bytes := p.payload[p.offset : p.offset+length] p.offset += length return bytes, nil }