Files
Piik/native/client/internal/loopback/protocol.go
T
AAA 5781d6bbfa feat(client): complete native source and release boundary
Complete the Native Client source/audio boundary, strict v5 loopback contract, Browser media lifecycle handling, and non-blocking release checks. Keep unverified platform/physical gates explicit; do not rewrite the Go Server.
2026-09-04 03:42:54 +08:00

74 lines
1.8 KiB
Go

package loopback
import (
"bytes"
"encoding/json"
"errors"
"io"
"regexp"
)
const (
ProtocolVersion = 5
ServiceName = "screener-client"
ControlSubprotocol = "screener-client-v5"
MaxControlMessageBytes = 64 << 10
DefaultPortStart = 39721
DefaultPortEnd = 39730
)
var (
requestIDPattern = regexp.MustCompile(`^[A-Za-z0-9_-]{8,64}$`)
errInvalidMessage = errors.New("loopback control message is invalid")
)
type controlMessage struct {
Version int `json:"version"`
ID string `json:"id"`
Type string `json:"type"`
}
func decodeEnvelope(payload []byte) (controlMessage, error) {
var message controlMessage
decoder := json.NewDecoder(bytes.NewReader(payload))
if decoder.Decode(&message) != nil || decoder.Decode(&struct{}{}) != io.EOF ||
message.Version != ProtocolVersion || !requestIDPattern.MatchString(message.ID) ||
message.Type == "" {
return controlMessage{}, errInvalidMessage
}
return message, nil
}
func decodeRequest(payload []byte) (controlMessage, error) {
var message controlMessage
decoder := json.NewDecoder(bytes.NewReader(payload))
decoder.DisallowUnknownFields()
if decoder.Decode(&message) != nil || decoder.Decode(&struct{}{}) != io.EOF ||
message.Version != ProtocolVersion || !requestIDPattern.MatchString(message.ID) {
return controlMessage{}, errInvalidMessage
}
return message, nil
}
func validateHello(message controlMessage) error {
if message.Type != "hello" {
return errInvalidMessage
}
return nil
}
func validatePing(message controlMessage) error {
if message.Type != "ping" {
return errInvalidMessage
}
return nil
}
func encodeMessage(value any) ([]byte, error) {
payload, err := json.Marshal(value)
if err != nil || len(payload) == 0 || len(payload) > MaxControlMessageBytes {
return nil, errInvalidMessage
}
return payload, nil
}