Some initial experimentation with SCPD reading.
This commit is contained in:
		
							
								
								
									
										167
									
								
								v2alpha/description/scpd/scpd.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								v2alpha/description/scpd/scpd.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,167 @@
 | 
			
		||||
// Package scpd contains data structures that represent an SCPD at a higher level than XML.
 | 
			
		||||
package scpd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"sort"
 | 
			
		||||
 | 
			
		||||
	"github.com/huin/goupnp/v2alpha/description/xmlscpd"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	BadDescriptionError         = errors.New("bad XML description")
 | 
			
		||||
	UnsupportedDescriptionError = errors.New("unsupported XML description")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// SCPD is the top level service description.
 | 
			
		||||
type SCPD struct {
 | 
			
		||||
	actionByName   map[string]*Action
 | 
			
		||||
	variableByName map[string]*StateVariable
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FromXML creates an SCPD from XML data.
 | 
			
		||||
//
 | 
			
		||||
// It assumes that xmlDesc.Clean() has been called.
 | 
			
		||||
func FromXML(xmlDesc *xmlscpd.SCPD) (*SCPD, error) {
 | 
			
		||||
	stateVariables := make(map[string]*StateVariable, len(xmlDesc.StateVariables))
 | 
			
		||||
	for _, xmlSV := range xmlDesc.StateVariables {
 | 
			
		||||
		sv, err := stateVariableFromXML(xmlSV)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("processing state variable %q: %w", xmlSV.Name, err)
 | 
			
		||||
		}
 | 
			
		||||
		if _, exists := stateVariables[sv.name]; exists {
 | 
			
		||||
			return nil, fmt.Errorf("%w: multiple state variables with name %q",
 | 
			
		||||
				BadDescriptionError, sv.name)
 | 
			
		||||
		}
 | 
			
		||||
		stateVariables[sv.name] = sv
 | 
			
		||||
	}
 | 
			
		||||
	actions := make(map[string]*Action, len(xmlDesc.Actions))
 | 
			
		||||
	for _, xmlAction := range xmlDesc.Actions {
 | 
			
		||||
		action, err := actionFromXML(xmlAction)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("processing action %q: %w", xmlAction.Name, err)
 | 
			
		||||
		}
 | 
			
		||||
		if _, exists := actions[action.name]; exists {
 | 
			
		||||
			return nil, fmt.Errorf("%w: multiple actions with name %q",
 | 
			
		||||
				BadDescriptionError, action.name)
 | 
			
		||||
		}
 | 
			
		||||
		actions[action.name] = action
 | 
			
		||||
	}
 | 
			
		||||
	return &SCPD{
 | 
			
		||||
		actionByName:   actions,
 | 
			
		||||
		variableByName: stateVariables,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ActionNames returns the ordered names of each action.
 | 
			
		||||
func (scpd *SCPD) ActionNames() []string {
 | 
			
		||||
	names := make([]string, 0, len(scpd.actionByName))
 | 
			
		||||
	for name := range scpd.actionByName {
 | 
			
		||||
		names = append(names, name)
 | 
			
		||||
	}
 | 
			
		||||
	sort.Strings(names)
 | 
			
		||||
	return names
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Action returns an action with the given name.
 | 
			
		||||
func (scpd *SCPD) Action(name string) *Action {
 | 
			
		||||
	return scpd.actionByName[name]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Variable returns a state variable with the given name.
 | 
			
		||||
func (scpd *SCPD) Variable(name string) *StateVariable {
 | 
			
		||||
	return scpd.variableByName[name]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Action describes a single UPnP SOAP action.
 | 
			
		||||
type Action struct {
 | 
			
		||||
	name    string
 | 
			
		||||
	inArgs  []*Argument
 | 
			
		||||
	outArgs []*Argument
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// actionFromXML creates an Action from the given XML description.
 | 
			
		||||
func actionFromXML(xmlAction *xmlscpd.Action) (*Action, error) {
 | 
			
		||||
	if xmlAction.Name == "" {
 | 
			
		||||
		return nil, fmt.Errorf("%w: empty action name", BadDescriptionError)
 | 
			
		||||
	}
 | 
			
		||||
	var inArgs []*Argument
 | 
			
		||||
	var outArgs []*Argument
 | 
			
		||||
	for _, xmlArg := range xmlAction.Arguments {
 | 
			
		||||
		arg, err := argumentFromXML(xmlArg)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("processing argument %q: %w", xmlArg.Name, err)
 | 
			
		||||
		}
 | 
			
		||||
		switch xmlArg.Direction {
 | 
			
		||||
		case "in":
 | 
			
		||||
			inArgs = append(inArgs, arg)
 | 
			
		||||
		case "out":
 | 
			
		||||
			outArgs = append(outArgs, arg)
 | 
			
		||||
		default:
 | 
			
		||||
			return nil, fmt.Errorf("%w: argument %q has invalid direction %q",
 | 
			
		||||
				BadDescriptionError, xmlArg.Name, xmlArg.Direction)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return &Action{
 | 
			
		||||
		name:    xmlAction.Name,
 | 
			
		||||
		inArgs:  inArgs,
 | 
			
		||||
		outArgs: outArgs,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Argument description data.
 | 
			
		||||
type Argument struct {
 | 
			
		||||
	name                 string
 | 
			
		||||
	relatedStateVariable string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// argumentFromXML creates an Argument from the XML description.
 | 
			
		||||
func argumentFromXML(xmlArg *xmlscpd.Argument) (*Argument, error) {
 | 
			
		||||
	if xmlArg.Name == "" {
 | 
			
		||||
		return nil, fmt.Errorf("%w: empty argument name", BadDescriptionError)
 | 
			
		||||
	}
 | 
			
		||||
	if xmlArg.RelatedStateVariable == "" {
 | 
			
		||||
		return nil, fmt.Errorf("%w: empty related state variable", BadDescriptionError)
 | 
			
		||||
	}
 | 
			
		||||
	return &Argument{
 | 
			
		||||
		name:                 xmlArg.Name,
 | 
			
		||||
		relatedStateVariable: xmlArg.RelatedStateVariable,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (arg *Argument) Name() string {
 | 
			
		||||
	return arg.name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (arg *Argument) RelatedStateVariableName() string {
 | 
			
		||||
	return arg.relatedStateVariable
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// StateVariable description data.
 | 
			
		||||
type StateVariable struct {
 | 
			
		||||
	name     string
 | 
			
		||||
	dataType string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func stateVariableFromXML(xmlSV *xmlscpd.StateVariable) (*StateVariable, error) {
 | 
			
		||||
	if xmlSV.Name == "" {
 | 
			
		||||
		return nil, fmt.Errorf("%w: empty state variable name", BadDescriptionError)
 | 
			
		||||
	}
 | 
			
		||||
	if xmlSV.DataType.Type != "" {
 | 
			
		||||
		return nil, fmt.Errorf("%w: unsupported data type %q",
 | 
			
		||||
			UnsupportedDescriptionError, xmlSV.DataType.Type)
 | 
			
		||||
	}
 | 
			
		||||
	return &StateVariable{
 | 
			
		||||
		name:     xmlSV.Name,
 | 
			
		||||
		dataType: xmlSV.DataType.Name,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (sv *StateVariable) Name() string {
 | 
			
		||||
	return sv.name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (sv *StateVariable) DataTypeName() string {
 | 
			
		||||
	return sv.dataType
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										161
									
								
								v2alpha/description/xmlscpd/xmlscpd.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								v2alpha/description/xmlscpd/xmlscpd.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,161 @@
 | 
			
		||||
// Package xmlscpd contains the XML data structures used in SCPD (Service Control Protocol Description).
 | 
			
		||||
//
 | 
			
		||||
// Described in section 2.5 of
 | 
			
		||||
// https://openconnectivity.org/upnp-specs/UPnP-arch-DeviceArchitecture-v2.0-20200417.pdf.
 | 
			
		||||
package xmlscpd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/xml"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func cleanWhitespace(s *string) {
 | 
			
		||||
	*s = strings.TrimSpace(*s)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SCPD is the top level XML service description.
 | 
			
		||||
type SCPD struct {
 | 
			
		||||
	XMLName        xml.Name         `xml:"scpd"`
 | 
			
		||||
	ConfigId       string           `xml:"configId,attr"`
 | 
			
		||||
	SpecVersion    SpecVersion      `xml:"specVersion"`
 | 
			
		||||
	Actions        []*Action        `xml:"actionList>action"`
 | 
			
		||||
	StateVariables []*StateVariable `xml:"serviceStateTable>stateVariable"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Clean removes stray whitespace in the structure.
 | 
			
		||||
//
 | 
			
		||||
// It's common for stray whitespace to be present in SCPD documents, this method removes them
 | 
			
		||||
// in-place.
 | 
			
		||||
func (scpd *SCPD) Clean() {
 | 
			
		||||
	cleanWhitespace(&scpd.ConfigId)
 | 
			
		||||
	for i := range scpd.Actions {
 | 
			
		||||
		scpd.Actions[i].Clean()
 | 
			
		||||
	}
 | 
			
		||||
	for i := range scpd.StateVariables {
 | 
			
		||||
		scpd.StateVariables[i].Clean()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SpecVersion is part of a SCPD document, describes the version of the
 | 
			
		||||
// specification that the data adheres to.
 | 
			
		||||
type SpecVersion struct {
 | 
			
		||||
	Major int32 `xml:"major"`
 | 
			
		||||
	Minor int32 `xml:"minor"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Action XML description data.
 | 
			
		||||
type Action struct {
 | 
			
		||||
	Name      string      `xml:"name"`
 | 
			
		||||
	Arguments []*Argument `xml:"argumentList>argument"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Clean removes stray whitespace in the structure.
 | 
			
		||||
func (action *Action) Clean() {
 | 
			
		||||
	cleanWhitespace(&action.Name)
 | 
			
		||||
	for i := range action.Arguments {
 | 
			
		||||
		action.Arguments[i].Clean()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Argument XML data.
 | 
			
		||||
type Argument struct {
 | 
			
		||||
	Name                 string `xml:"name"`
 | 
			
		||||
	Direction            string `xml:"direction"`            // in|out
 | 
			
		||||
	RelatedStateVariable string `xml:"relatedStateVariable"` // ?
 | 
			
		||||
	Retval               string `xml:"retval"`               // ?
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Clean removes stray whitespace in the structure.
 | 
			
		||||
func (arg *Argument) Clean() {
 | 
			
		||||
	cleanWhitespace(&arg.Name)
 | 
			
		||||
	cleanWhitespace(&arg.Direction)
 | 
			
		||||
	cleanWhitespace(&arg.RelatedStateVariable)
 | 
			
		||||
	cleanWhitespace(&arg.Retval)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// StateVariable XML data.
 | 
			
		||||
type StateVariable struct {
 | 
			
		||||
	Optional          PresenceBool
 | 
			
		||||
	Name              string             `xml:"name"`
 | 
			
		||||
	SendEvents        string             `xml:"sendEvents,attr"` // yes|no
 | 
			
		||||
	Multicast         string             `xml:"multicast,attr"`  // yes|no
 | 
			
		||||
	DataType          DataType           `xml:"dataType"`
 | 
			
		||||
	DefaultValue      string             `xml:"defaultValue"`
 | 
			
		||||
	AllowedValueRange *AllowedValueRange `xml:"allowedValueRange"`
 | 
			
		||||
	AllowedValues     []string           `xml:"allowedValueList>allowedValue"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Clean removes stray whitespace in the structure.
 | 
			
		||||
func (v *StateVariable) Clean() {
 | 
			
		||||
	cleanWhitespace(&v.Name)
 | 
			
		||||
	cleanWhitespace(&v.SendEvents)
 | 
			
		||||
	cleanWhitespace(&v.Multicast)
 | 
			
		||||
	v.DataType.Clean()
 | 
			
		||||
	cleanWhitespace(&v.DefaultValue)
 | 
			
		||||
	if v.AllowedValueRange != nil {
 | 
			
		||||
		v.AllowedValueRange.Clean()
 | 
			
		||||
	}
 | 
			
		||||
	for i := range v.AllowedValues {
 | 
			
		||||
		cleanWhitespace(&v.AllowedValues[i])
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AllowedValueRange XML data.
 | 
			
		||||
type AllowedValueRange struct {
 | 
			
		||||
	Minimum string `xml:"minimum"`
 | 
			
		||||
	Maximum string `xml:"maximum"`
 | 
			
		||||
	Step    string `xml:"step"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Clean removes stray whitespace in the structure.
 | 
			
		||||
func (r *AllowedValueRange) Clean() {
 | 
			
		||||
	cleanWhitespace(&r.Minimum)
 | 
			
		||||
	cleanWhitespace(&r.Maximum)
 | 
			
		||||
	cleanWhitespace(&r.Step)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DataType XML data.
 | 
			
		||||
type DataType struct {
 | 
			
		||||
	Name string `xml:",chardata"`
 | 
			
		||||
	Type string `xml:"type,attr"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Clean removes stray whitespace in the structure.
 | 
			
		||||
func (dt *DataType) Clean() {
 | 
			
		||||
	cleanWhitespace(&dt.Name)
 | 
			
		||||
	cleanWhitespace(&dt.Type)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PresenceBool represents an empty XML element that is true if present.
 | 
			
		||||
//
 | 
			
		||||
// Is an error if it contains any attributes or contents.
 | 
			
		||||
type PresenceBool bool
 | 
			
		||||
 | 
			
		||||
func (pb *PresenceBool) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
 | 
			
		||||
	*pb = true
 | 
			
		||||
	if len(start.Attr) > 0 {
 | 
			
		||||
		return fmt.Errorf("unexpected attributes on element %s:%s",
 | 
			
		||||
			start.Name.Space, start.Name.Local)
 | 
			
		||||
	}
 | 
			
		||||
	for {
 | 
			
		||||
		tok, err := d.Token()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		switch tok := tok.(type) {
 | 
			
		||||
		case xml.CharData:
 | 
			
		||||
			if len(bytes.TrimSpace([]byte(tok))) > 0 {
 | 
			
		||||
				return fmt.Errorf("unexpected char data on element %s:%s",
 | 
			
		||||
					start.Name.Space, start.Name.Local)
 | 
			
		||||
			}
 | 
			
		||||
		case xml.EndElement:
 | 
			
		||||
			return nil
 | 
			
		||||
		case xml.Comment:
 | 
			
		||||
		default:
 | 
			
		||||
			return fmt.Errorf("unexpected %T token on element %s:%s",
 | 
			
		||||
				tok, start.Name.Space, start.Name.Local)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user