mirror of
https://github.com/onyx-and-iris/xair-cli.git
synced 2026-02-04 07:27:47 +00:00
217 lines
5.7 KiB
Go
217 lines
5.7 KiB
Go
/*
|
|
LICENSE: https://github.com/onyx-and-iris/xair-cli/blob/main/LICENSE
|
|
*/
|
|
package xair
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"math"
|
|
|
|
"github.com/charmbracelet/log"
|
|
"github.com/hypebeast/go-osc/osc"
|
|
)
|
|
|
|
type xairParser struct {
|
|
}
|
|
|
|
func newParser() *xairParser {
|
|
return &xairParser{}
|
|
}
|
|
|
|
// parseOSCMessage parses raw bytes into an OSC message with improved error handling
|
|
func (p *xairParser) Parse(data []byte) (*osc.Message, error) {
|
|
log.Debug("=== PARSING OSC MESSAGE BEGIN ===")
|
|
defer log.Debug("=== PARSING OSC MESSAGE END ===")
|
|
|
|
if err := p.validateOSCData(data); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
address, addressEnd, err := p.extractOSCAddress(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
msg := osc.NewMessage(address)
|
|
|
|
typeTags, typeTagsEnd, err := p.extractOSCTypeTags(data, addressEnd)
|
|
if err != nil || typeTags == "" {
|
|
log.Debug("No valid type tags, returning address-only message")
|
|
return msg, nil
|
|
}
|
|
|
|
if err := p.parseOSCArguments(data, typeTagsEnd, typeTags, msg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log.Debugf("Successfully parsed message with %d arguments", len(msg.Arguments))
|
|
return msg, nil
|
|
}
|
|
|
|
// validateOSCData performs basic validation on OSC message data
|
|
func (p *xairParser) validateOSCData(data []byte) error {
|
|
if len(data) < 4 {
|
|
return fmt.Errorf("data too short for OSC message")
|
|
}
|
|
if data[0] != '/' {
|
|
return fmt.Errorf("invalid OSC message: does not start with '/'")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// extractOSCAddress extracts the OSC address from the message data
|
|
func (p *xairParser) extractOSCAddress(data []byte) (address string, nextPos int, err error) {
|
|
nullPos := bytes.IndexByte(data, 0)
|
|
if nullPos <= 0 {
|
|
return "", 0, fmt.Errorf("no null terminator found for address")
|
|
}
|
|
|
|
address = string(data[:nullPos])
|
|
log.Debugf("Parsed OSC address: %s", address)
|
|
|
|
// Calculate next 4-byte aligned position
|
|
nextPos = ((nullPos + 4) / 4) * 4
|
|
return address, nextPos, nil
|
|
}
|
|
|
|
// extractOSCTypeTags extracts and validates OSC type tags
|
|
func (p *xairParser) extractOSCTypeTags(data []byte, start int) (typeTags string, nextPos int, err error) {
|
|
if start >= len(data) {
|
|
return "", start, nil // No type tags available
|
|
}
|
|
|
|
typeTagsEnd := bytes.IndexByte(data[start:], 0)
|
|
if typeTagsEnd <= 0 {
|
|
return "", start, nil // No type tags found
|
|
}
|
|
|
|
typeTags = string(data[start : start+typeTagsEnd])
|
|
log.Debugf("Parsed type tags: %s", typeTags)
|
|
|
|
if len(typeTags) == 0 || typeTags[0] != ',' {
|
|
log.Debug("Invalid type tags format")
|
|
return "", start, nil
|
|
}
|
|
|
|
// Calculate arguments start position (4-byte aligned)
|
|
nextPos = ((start + typeTagsEnd + 4) / 4) * 4
|
|
return typeTags, nextPos, nil
|
|
}
|
|
|
|
// parseOSCArguments parses OSC arguments based on type tags
|
|
func (p *xairParser) parseOSCArguments(data []byte, argsStart int, typeTags string, msg *osc.Message) error {
|
|
argData := data[argsStart:]
|
|
argNum := 0
|
|
|
|
for i := 1; i < len(typeTags) && len(argData) > 0; i++ {
|
|
var consumed int
|
|
var err error
|
|
|
|
switch typeTags[i] {
|
|
case 's':
|
|
consumed, err = p.parseStringArgument(argData, msg, argNum)
|
|
case 'i':
|
|
consumed, err = p.parseInt32Argument(argData, msg, argNum)
|
|
case 'f':
|
|
consumed, err = p.parseFloat32Argument(argData, msg, argNum)
|
|
case 'b':
|
|
consumed, err = p.parseBlobArgument(argData, msg, argNum)
|
|
default:
|
|
log.Debugf("Unknown type tag: %c (skipping)", typeTags[i])
|
|
consumed = p.skipUnknownArgument(argData)
|
|
}
|
|
|
|
if err != nil {
|
|
log.Debugf("Error parsing argument %d: %v", argNum+1, err)
|
|
break
|
|
}
|
|
|
|
if consumed == 0 {
|
|
break // No more data to consume
|
|
}
|
|
|
|
argData = argData[consumed:]
|
|
if typeTags[i] != '?' { // Don't count skipped arguments
|
|
argNum++
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// parseStringArgument parses a string argument from OSC data
|
|
func (p *xairParser) parseStringArgument(data []byte, msg *osc.Message, argNum int) (int, error) {
|
|
nullPos := bytes.IndexByte(data, 0)
|
|
if nullPos < 0 {
|
|
return 0, fmt.Errorf("no null terminator found for string")
|
|
}
|
|
|
|
argStr := string(data[:nullPos])
|
|
log.Debugf("Parsed string argument %d: %s", argNum+1, argStr)
|
|
msg.Append(argStr)
|
|
|
|
// Return next 4-byte aligned position
|
|
return ((nullPos + 4) / 4) * 4, nil
|
|
}
|
|
|
|
// parseInt32Argument parses an int32 argument from OSC data
|
|
func (p *xairParser) parseInt32Argument(data []byte, msg *osc.Message, argNum int) (int, error) {
|
|
if len(data) < 4 {
|
|
return 0, fmt.Errorf("insufficient data for int32")
|
|
}
|
|
|
|
val := int32(binary.BigEndian.Uint32(data[:4]))
|
|
log.Debugf("Parsed int32 argument %d: %d", argNum+1, val)
|
|
msg.Append(val)
|
|
|
|
return 4, nil
|
|
}
|
|
|
|
// parseFloat32Argument parses a float32 argument from OSC data
|
|
func (p *xairParser) parseFloat32Argument(data []byte, msg *osc.Message, argNum int) (int, error) {
|
|
if len(data) < 4 {
|
|
return 0, fmt.Errorf("insufficient data for float32")
|
|
}
|
|
|
|
val := math.Float32frombits(binary.BigEndian.Uint32(data[:4]))
|
|
log.Debugf("Parsed float32 argument %d: %f", argNum+1, val)
|
|
msg.Append(val)
|
|
|
|
return 4, nil
|
|
}
|
|
|
|
// parseBlobArgument parses a blob argument from OSC data
|
|
func (p *xairParser) parseBlobArgument(data []byte, msg *osc.Message, argNum int) (int, error) {
|
|
if len(data) < 4 {
|
|
return 0, fmt.Errorf("insufficient data for blob size")
|
|
}
|
|
|
|
size := int32(binary.BigEndian.Uint32(data[:4]))
|
|
if size < 0 || size >= 10000 {
|
|
return 0, fmt.Errorf("invalid blob size: %d", size)
|
|
}
|
|
|
|
if len(data) < int(4+size) {
|
|
return 0, fmt.Errorf("insufficient data for blob content")
|
|
}
|
|
|
|
blob := make([]byte, size)
|
|
copy(blob, data[4:4+size])
|
|
log.Debugf("Parsed blob argument %d (%d bytes)", argNum+1, size)
|
|
msg.Append(blob)
|
|
|
|
// Return next 4-byte aligned position
|
|
return ((4 + int(size) + 3) / 4) * 4, nil
|
|
}
|
|
|
|
// skipUnknownArgument skips an unknown argument type
|
|
func (p *xairParser) skipUnknownArgument(data []byte) int {
|
|
// Skip unknown types by moving 4 bytes if available
|
|
if len(data) >= 4 {
|
|
return 4
|
|
}
|
|
return 0
|
|
}
|