fetch_ml/internal/api/helpers/payload_parser.go
Jeremie Fraeys b05470b30a
refactor: improve API structure and WebSocket protocol
- Extract WebSocket protocol handling to dedicated module
- Add helper functions for DB operations, validation, and responses
- Improve WebSocket frame handling and opcodes
- Refactor dataset, job, and Jupyter handlers
- Add duplicate detection processing
2026-02-16 20:38:12 -05:00

121 lines
3.5 KiB
Go

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