226 lines
6.8 KiB
Go
Raw Normal View History

2026-03-12 20:51:38 -07:00
package float
import (
"encoding/binary"
"fmt"
)
// Frame type constants define the binary protocol for USB passthrough over WebSocket.
const (
FrameEnumerate byte = 0x01
FrameEnumResult byte = 0x02
FrameOpen byte = 0x03
FrameOpenResult byte = 0x04
FrameClose byte = 0x05
FrameCloseResult byte = 0x06
FrameTransferOut byte = 0x10
FrameTransferIn byte = 0x11
FrameTransferResult byte = 0x12
FrameInterrupt byte = 0x20
FrameInterruptResult byte = 0x21
FramePing byte = 0xFE
FramePong byte = 0xFF
FrameError byte = 0xE0
)
// frameHeaderSize is the fixed size of a frame header: 1 byte type + 4 bytes length.
const frameHeaderSize = 5
// USBDevice represents a USB device detected on the client host.
type USBDevice struct {
VendorID uint16 `json:"vendor_id"`
ProductID uint16 `json:"product_id"`
DeviceID uint16 `json:"device_id"`
Manufacturer string `json:"manufacturer"`
Product string `json:"product"`
SerialNumber string `json:"serial_number"`
Class byte `json:"class"`
SubClass byte `json:"sub_class"`
}
// deviceFixedSize is the fixed portion of a serialized USBDevice:
// VendorID(2) + ProductID(2) + DeviceID(2) + Class(1) + SubClass(1) + 3 string lengths (2 each) = 14
const deviceFixedSize = 14
// EncodeFrame builds a binary frame: [type:1][length:4 big-endian][payload:N].
func EncodeFrame(frameType byte, payload []byte) []byte {
frame := make([]byte, frameHeaderSize+len(payload))
frame[0] = frameType
binary.BigEndian.PutUint32(frame[1:5], uint32(len(payload)))
copy(frame[frameHeaderSize:], payload)
return frame
}
// DecodeFrame parses a binary frame into its type and payload.
func DecodeFrame(data []byte) (frameType byte, payload []byte, err error) {
if len(data) < frameHeaderSize {
return 0, nil, fmt.Errorf("frame too short: need at least %d bytes, got %d", frameHeaderSize, len(data))
}
frameType = data[0]
length := binary.BigEndian.Uint32(data[1:5])
if uint32(len(data)-frameHeaderSize) < length {
return 0, nil, fmt.Errorf("frame payload truncated: header says %d bytes, have %d", length, len(data)-frameHeaderSize)
}
payload = make([]byte, length)
copy(payload, data[frameHeaderSize:frameHeaderSize+int(length)])
return frameType, payload, nil
}
// encodeString writes a length-prefixed string (2-byte big-endian length + bytes).
func encodeString(buf []byte, offset int, s string) int {
b := []byte(s)
binary.BigEndian.PutUint16(buf[offset:], uint16(len(b)))
offset += 2
copy(buf[offset:], b)
return offset + len(b)
}
// decodeString reads a length-prefixed string from the buffer.
func decodeString(data []byte, offset int) (string, int, error) {
if offset+2 > len(data) {
return "", 0, fmt.Errorf("string length truncated at offset %d", offset)
}
slen := int(binary.BigEndian.Uint16(data[offset:]))
offset += 2
if offset+slen > len(data) {
return "", 0, fmt.Errorf("string data truncated at offset %d: need %d bytes", offset, slen)
}
s := string(data[offset : offset+slen])
return s, offset + slen, nil
}
// serializeDevice serializes a single USBDevice into bytes.
func serializeDevice(dev USBDevice) []byte {
mfr := []byte(dev.Manufacturer)
prod := []byte(dev.Product)
ser := []byte(dev.SerialNumber)
size := deviceFixedSize + len(mfr) + len(prod) + len(ser)
buf := make([]byte, size)
binary.BigEndian.PutUint16(buf[0:], dev.VendorID)
binary.BigEndian.PutUint16(buf[2:], dev.ProductID)
binary.BigEndian.PutUint16(buf[4:], dev.DeviceID)
buf[6] = dev.Class
buf[7] = dev.SubClass
off := 8
off = encodeString(buf, off, dev.Manufacturer)
off = encodeString(buf, off, dev.Product)
_ = encodeString(buf, off, dev.SerialNumber)
return buf
}
// EncodeDeviceList serializes a slice of USBDevices for a FrameEnumResult payload.
// Format: [count:2 big-endian][device...]
func EncodeDeviceList(devices []USBDevice) []byte {
// First pass: serialize each device to compute total size
serialized := make([][]byte, len(devices))
totalSize := 2 // 2 bytes for count
for i, dev := range devices {
serialized[i] = serializeDevice(dev)
totalSize += len(serialized[i])
}
buf := make([]byte, totalSize)
binary.BigEndian.PutUint16(buf[0:], uint16(len(devices)))
off := 2
for _, s := range serialized {
copy(buf[off:], s)
off += len(s)
}
return buf
}
// DecodeDeviceList deserializes a FrameEnumResult payload into a slice of USBDevices.
func DecodeDeviceList(data []byte) ([]USBDevice, error) {
if len(data) < 2 {
return nil, fmt.Errorf("device list too short: need at least 2 bytes")
}
count := int(binary.BigEndian.Uint16(data[0:]))
off := 2
devices := make([]USBDevice, 0, count)
for i := 0; i < count; i++ {
if off+8 > len(data) {
return nil, fmt.Errorf("device %d: fixed fields truncated at offset %d", i, off)
}
dev := USBDevice{
VendorID: binary.BigEndian.Uint16(data[off:]),
ProductID: binary.BigEndian.Uint16(data[off+2:]),
DeviceID: binary.BigEndian.Uint16(data[off+4:]),
Class: data[off+6],
SubClass: data[off+7],
}
off += 8
var err error
dev.Manufacturer, off, err = decodeString(data, off)
if err != nil {
return nil, fmt.Errorf("device %d manufacturer: %w", i, err)
}
dev.Product, off, err = decodeString(data, off)
if err != nil {
return nil, fmt.Errorf("device %d product: %w", i, err)
}
dev.SerialNumber, off, err = decodeString(data, off)
if err != nil {
return nil, fmt.Errorf("device %d serial: %w", i, err)
}
devices = append(devices, dev)
}
return devices, nil
}
// EncodeTransfer serializes a USB transfer payload.
// Format: [deviceID:2][endpoint:1][data:N]
func EncodeTransfer(deviceID uint16, endpoint byte, data []byte) []byte {
buf := make([]byte, 3+len(data))
binary.BigEndian.PutUint16(buf[0:], deviceID)
buf[2] = endpoint
copy(buf[3:], data)
return buf
}
// DecodeTransfer deserializes a USB transfer payload.
func DecodeTransfer(data []byte) (deviceID uint16, endpoint byte, transferData []byte, err error) {
if len(data) < 3 {
return 0, 0, nil, fmt.Errorf("transfer payload too short: need at least 3 bytes, got %d", len(data))
}
deviceID = binary.BigEndian.Uint16(data[0:])
endpoint = data[2]
transferData = make([]byte, len(data)-3)
copy(transferData, data[3:])
return deviceID, endpoint, transferData, nil
}
// EncodeError serializes an error response payload.
// Format: [code:2 big-endian][message:UTF-8 bytes]
func EncodeError(code uint16, message string) []byte {
msg := []byte(message)
buf := make([]byte, 2+len(msg))
binary.BigEndian.PutUint16(buf[0:], code)
copy(buf[2:], msg)
return buf
}
// DecodeError deserializes an error response payload.
func DecodeError(data []byte) (code uint16, message string) {
if len(data) < 2 {
return 0, ""
}
code = binary.BigEndian.Uint16(data[0:])
message = string(data[2:])
return code, message
}