Files
T

230 lines
4.7 KiB
Go

package ctl
import (
"bytes"
"encoding/xml"
"errors"
"io"
)
var errNoElement = errors.New("ctl: stanza has no root element")
// Kind classifies an inbound stanza.
type Kind uint8
const (
KindUnknown Kind = iota
// KindAck is an iq result whose id correlates a stanza ack (sid).
KindAck
// KindResult is an iq set whose ctl carries ret and id (cid).
KindResult
// KindPush is an unsolicited iq set whose ctl carries td and no ret.
KindPush
// KindBattery is an iq set whose query holds a bare battery element.
KindBattery
)
// Inbound is one parsed inbound stanza. Inner is the raw ctl inner XML.
// Errno is a pointer so an omitted errno differs from an empty one.
type Inbound struct {
Kind Kind
SID string
CID string
TD string
Ret string
Errno *string
Attrs map[string]string
Inner []byte
BatteryPower string
CleanAttrs map[string]string
ChargeAttrs map[string]string
}
// Parse classifies one stanza. Elements are matched by local name so both
// namespaced and unqualified iq/query forms parse. Well-formed stanzas that
// match no known shape return KindUnknown; malformed XML returns an error.
func Parse(stanza []byte) (Inbound, error) {
var in Inbound
dec := xml.NewDecoder(bytes.NewReader(stanza))
root, err := nextStart(dec)
if err != nil {
return in, err
}
if root == nil {
return in, errNoElement
}
if root.Name.Local != "iq" {
return in, drain(dec)
}
iqType := findAttr(root, "type")
switch iqType {
case "result":
if id := findAttr(root, "id"); id != "" {
in.Kind = KindAck
in.SID = id
}
return in, drain(dec)
case "set":
default:
return in, drain(dec)
}
query, err := nextStart(dec)
if err != nil {
return in, err
}
if query == nil || query.Name.Local != "query" {
return in, drain(dec)
}
child, err := nextStart(dec)
if err != nil {
return in, err
}
if child == nil {
return in, drain(dec)
}
switch child.Name.Local {
case "ctl":
if err := parseCtl(dec, child, &in); err != nil {
return in, err
}
case "battery":
in.Kind = KindBattery
in.BatteryPower = findAttr(child, "power")
}
return in, drain(dec)
}
func parseCtl(dec *xml.Decoder, start *xml.StartElement, in *Inbound) error {
attrs := make(map[string]string, len(start.Attr))
for _, a := range start.Attr {
attrs[a.Name.Local] = a.Value
}
in.Attrs = attrs
if v, ok := attrs["errno"]; ok {
vv := v
in.Errno = &vv
}
var inner struct {
Raw []byte `xml:",innerxml"`
}
if err := dec.DecodeElement(&inner, start); err != nil {
return err
}
in.Inner = inner.Raw
td := attrs["td"]
cid := attrs["id"]
ret := attrs["ret"]
switch {
case ret != "" && cid != "":
in.Kind = KindResult
in.CID = cid
in.Ret = ret
case td != "" && ret == "":
in.Kind = KindPush
in.TD = td
default:
in.Kind = KindUnknown
}
return parseInner(inner.Raw, in)
}
// parseInner records the attributes of first-level payload elements inside the
// ctl inner XML: clean, charge and battery.
func parseInner(raw []byte, in *Inbound) error {
if len(bytes.TrimSpace(raw)) == 0 {
return nil
}
doc := make([]byte, 0, len(raw)+7)
doc = append(doc, '<', 'x', '>')
doc = append(doc, raw...)
doc = append(doc, '<', '/', 'x', '>')
dec := xml.NewDecoder(bytes.NewReader(doc))
depth := 0
for {
tok, err := dec.Token()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
switch t := tok.(type) {
case xml.StartElement:
depth++
if depth == 2 {
switch t.Name.Local {
case "clean":
if in.CleanAttrs == nil {
in.CleanAttrs = attrsOf(&t)
}
case "charge":
if in.ChargeAttrs == nil {
in.ChargeAttrs = attrsOf(&t)
}
case "battery":
if in.BatteryPower == "" {
in.BatteryPower = findAttr(&t, "power")
}
}
}
case xml.EndElement:
depth--
}
}
}
func attrsOf(se *xml.StartElement) map[string]string {
m := make(map[string]string, len(se.Attr))
for _, a := range se.Attr {
m[a.Name.Local] = a.Value
}
return m
}
func findAttr(se *xml.StartElement, name string) string {
for _, a := range se.Attr {
if a.Name.Local == name {
return a.Value
}
}
return ""
}
// nextStart returns the next start element, skipping text nodes. It returns
// nil on EOF or on an end element.
func nextStart(dec *xml.Decoder) (*xml.StartElement, error) {
for {
tok, err := dec.Token()
if err == io.EOF {
return nil, nil
}
if err != nil {
return nil, err
}
switch t := tok.(type) {
case xml.StartElement:
return &t, nil
case xml.EndElement:
return nil, nil
}
}
}
// drain consumes the rest of the stanza so trailing malformed XML is an error.
func drain(dec *xml.Decoder) error {
for {
_, err := dec.Token()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
}
}