// Package contracts defines the isolated test payloads, not the existing device protocol. package contracts import ( "bytes" "encoding/json" "fmt" "io" "math" "regexp" "strings" "time" "unicode/utf8" ) const MaxPayloadBytes = 16384 const MaxCommandLifetime = time.Minute var safeID = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$`) type Envelope struct { SchemaVersion int `json:"schema_version"` DeviceID string `json:"device_id"` } type Telemetry struct { Envelope MessageID string `json:"message_id"` BootID string `json:"boot_id"` Sequence *uint64 `json:"sequence"` Timestamp string `json:"timestamp"` Readings map[string]float64 `json:"readings"` } type Status struct { Envelope BootID string `json:"boot_id"` State string `json:"state"` Timestamp string `json:"timestamp,omitempty"` WillConfiguredAt string `json:"will_configured_at,omitempty"` Implementation string `json:"implementation,omitempty"` ReportedProtocol string `json:"reported_protocol,omitempty"` } type Command struct { Envelope RequestID string `json:"request_id"` IssuedAt string `json:"issued_at"` ExpiresAt string `json:"expires_at"` Command string `json:"command"` } type Result struct { Pong *bool `json:"pong,omitempty"` Version string `json:"version,omitempty"` } type AckError struct { Code string `json:"code"` Message string `json:"message"` } type Ack struct { Envelope RequestID string `json:"request_id"` Timestamp string `json:"timestamp"` Stage string `json:"stage"` OK *bool `json:"ok"` Result *Result `json:"result,omitempty"` Error *AckError `json:"error,omitempty"` } func (e Envelope) validate(device string) error { if e.SchemaVersion != 1 { return fmt.Errorf("schema_version: expected 1") } if !safeID.MatchString(e.DeviceID) || e.DeviceID != device { return fmt.Errorf("device_id: invalid or mismatched topic identity") } return nil } func id(name, value string) error { if !safeID.MatchString(value) { return fmt.Errorf("%s: expected 1..64 safe identifier characters", name) } return nil } func timestamp(name, value string) (time.Time, error) { t, err := time.Parse(time.RFC3339Nano, value) if err != nil || !strings.HasSuffix(value, "Z") { return time.Time{}, fmt.Errorf("%s: expected RFC3339 UTC timestamp ending Z", name) } return t, nil } func (m Telemetry) Validate(device string) error { if err := m.Envelope.validate(device); err != nil { return err } if err := id("message_id", m.MessageID); err != nil { return err } if err := id("boot_id", m.BootID); err != nil { return err } if _, err := timestamp("timestamp", m.Timestamp); err != nil { return err } if m.Sequence == nil { return fmt.Errorf("sequence: required") } if len(m.Readings) == 0 || len(m.Readings) > 16 { return fmt.Errorf("readings: expected 1..16 numeric readings") } for key, value := range m.Readings { if !safeID.MatchString(key) || math.IsNaN(value) || math.IsInf(value, 0) { return fmt.Errorf("readings: invalid name or nonfinite value") } } return nil } func (m Status) Validate(device string) error { if err := m.Envelope.validate(device); err != nil { return err } if err := id("boot_id", m.BootID); err != nil { return err } if m.State != "online" && m.State != "offline" { return fmt.Errorf("state: expected online or offline") } if m.Timestamp != "" && m.WillConfiguredAt != "" { return fmt.Errorf("timestamp: cannot be combined with will_configured_at") } if m.WillConfiguredAt != "" { if m.State != "offline" { return fmt.Errorf("will_configured_at: only valid for offline Will") } if _, err := timestamp("will_configured_at", m.WillConfiguredAt); err != nil { return err } } else { if _, err := timestamp("timestamp", m.Timestamp); err != nil { return err } } if m.Implementation != "" { if err := id("implementation", m.Implementation); err != nil { return err } } if m.ReportedProtocol != "" && m.ReportedProtocol != "5" && m.ReportedProtocol != "3.1.1" { return fmt.Errorf("reported_protocol: expected 5 or 3.1.1") } return nil } func (m Command) Validate(device string) error { if err := m.Envelope.validate(device); err != nil { return err } if err := id("request_id", m.RequestID); err != nil { return err } if m.Command != "ping" && m.Command != "get_version" { return fmt.Errorf("command: only ping or get_version allowed") } issued, err := timestamp("issued_at", m.IssuedAt) if err != nil { return err } expires, err := timestamp("expires_at", m.ExpiresAt) if err != nil { return err } lifetime := expires.Sub(issued) if lifetime < time.Second || lifetime > MaxCommandLifetime { return fmt.Errorf("expires_at: lifetime must be 1s..1m after issued_at") } return nil } // CheckDeadline is deliberately separate from structural validation, for delayed/replayed messages. func (m Command) CheckDeadline(now time.Time) error { if err := m.Validate(m.DeviceID); err != nil { return err } issued, _ := timestamp("issued_at", m.IssuedAt) expires, _ := timestamp("expires_at", m.ExpiresAt) if now.Before(issued) { return fmt.Errorf("issued_at: command is from the future; check device clock") } if !now.Before(expires) { return fmt.Errorf("expires_at: command expired") } return nil } func (m Ack) Validate(device string) error { if err := m.Envelope.validate(device); err != nil { return err } if err := id("request_id", m.RequestID); err != nil { return err } if _, err := timestamp("timestamp", m.Timestamp); err != nil { return err } if m.OK == nil { return fmt.Errorf("ok: boolean is required") } switch m.Stage { case "accepted": if !*m.OK || m.Result != nil || m.Error != nil { return fmt.Errorf("accepted: requires ok=true without result/error") } case "completed": if !*m.OK || m.Error != nil || m.Result == nil { return fmt.Errorf("completed: requires ok=true and result without error") } if (m.Result.Pong != nil) == (m.Result.Version != "") { return fmt.Errorf("result: expected exactly one of pong or version") } if m.Result.Pong != nil && !*m.Result.Pong { return fmt.Errorf("result.pong: expected true") } if m.Result.Version != "" && !regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$`).MatchString(m.Result.Version) { return fmt.Errorf("result.version: invalid or too long") } case "rejected", "failed": if *m.OK || m.Error == nil || m.Result != nil { return fmt.Errorf("rejected/failed: requires ok=false and error without result") } if err := id("error.code", m.Error.Code); err != nil { return err } if m.Error.Message == "" || len(m.Error.Message) > 256 { return fmt.Errorf("error.message: expected 1..256 bytes") } default: return fmt.Errorf("stage: expected accepted, completed, rejected or failed") } return nil } // Decode rejects duplicate keys, unknown fields, invalid UTF-8 and trailing values. // Errors contain no payload contents. The returned concrete type implements Validate. func Decode(kind string, data []byte, device string) (any, error) { if len(data) > MaxPayloadBytes || len(data) == 0 { return nil, fmt.Errorf("payload: expected 1..16384 bytes") } if !utf8.Valid(data) { return nil, fmt.Errorf("payload: invalid UTF-8") } if err := uniqueObject(data); err != nil { return nil, err } var target interface{ Validate(string) error } switch kind { case "telemetry": target = &Telemetry{} case "status": target = &Status{} case "commands": target = &Command{} case "acks": target = &Ack{} default: return nil, fmt.Errorf("topic: unsupported message category") } dec := json.NewDecoder(bytes.NewReader(data)) dec.DisallowUnknownFields() if err := dec.Decode(target); err != nil { return nil, fmt.Errorf("payload: invalid JSON field or type") } if err := target.Validate(device); err != nil { return nil, err } return target, nil } func uniqueObject(data []byte) error { d := json.NewDecoder(bytes.NewReader(data)) d.UseNumber() first, err := d.Token() if err != nil || first != json.Delim('{') { return fmt.Errorf("payload: expected JSON object") } if err = walkObject(d, 0); err != nil { return fmt.Errorf("payload: malformed JSON, duplicate key or excessive nesting") } if _, err = d.Token(); err != io.EOF { return fmt.Errorf("payload: trailing JSON data") } return nil } func walkObject(d *json.Decoder, depth int) error { if depth > 8 { return fmt.Errorf("depth") } seen := map[string]bool{} for d.More() { token, err := d.Token() if err != nil { return err } key, ok := token.(string) // encoding/json matches struct fields case-insensitively. Reject aliases // of an existing key as well as byte-identical duplicates. canonical := strings.ToLower(key) if !ok || seen[canonical] { return fmt.Errorf("duplicate") } seen[canonical] = true t, err := d.Token() if err != nil { return err } if t == nil { return fmt.Errorf("null values unsupported") } if delim, ok := t.(json.Delim); ok { if delim != '{' { return fmt.Errorf("arrays unsupported") } if err = walkObject(d, depth+1); err != nil { return err } } } end, err := d.Token() if err != nil || end != json.Delim('}') { return fmt.Errorf("object") } return nil }