230 lines
4.7 KiB
Go
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
|
|
}
|
|
}
|
|
}
|