2022-05-27 06:00:09 +00:00
|
|
|
// Package srvdesc contains data structures that represent an SCPD at a higher level than XML.
|
|
|
|
package srvdesc
|
2022-05-25 17:27:18 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"sort"
|
|
|
|
|
2022-05-27 06:00:09 +00:00
|
|
|
"github.com/huin/goupnp/v2alpha/description/xmlsrvdesc"
|
2022-05-25 17:27:18 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2023-03-09 17:08:45 +00:00
|
|
|
ErrBadDescription = errors.New("bad XML description")
|
|
|
|
ErrMissingDefinition = errors.New("missing definition")
|
|
|
|
ErrUnsupportedDescription = errors.New("unsupported XML description")
|
2022-05-25 17:27:18 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// SCPD is the top level service description.
|
|
|
|
type SCPD struct {
|
2022-06-06 17:00:39 +00:00
|
|
|
ActionByName map[string]*Action
|
|
|
|
VariableByName map[string]*StateVariable
|
2022-05-25 17:27:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// FromXML creates an SCPD from XML data.
|
|
|
|
//
|
|
|
|
// It assumes that xmlDesc.Clean() has been called.
|
2022-05-27 06:00:09 +00:00
|
|
|
func FromXML(xmlDesc *xmlsrvdesc.SCPD) (*SCPD, error) {
|
2022-06-06 17:00:39 +00:00
|
|
|
scpd := &SCPD{
|
|
|
|
ActionByName: make(map[string]*Action, len(xmlDesc.Actions)),
|
|
|
|
VariableByName: make(map[string]*StateVariable, len(xmlDesc.StateVariables)),
|
|
|
|
}
|
|
|
|
stateVariables := scpd.VariableByName
|
2022-05-25 17:27:18 +00:00
|
|
|
for _, xmlSV := range xmlDesc.StateVariables {
|
|
|
|
sv, err := stateVariableFromXML(xmlSV)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("processing state variable %q: %w", xmlSV.Name, err)
|
|
|
|
}
|
2022-06-06 17:00:39 +00:00
|
|
|
if _, exists := stateVariables[sv.Name]; exists {
|
2022-05-25 17:27:18 +00:00
|
|
|
return nil, fmt.Errorf("%w: multiple state variables with name %q",
|
2023-03-09 17:08:45 +00:00
|
|
|
ErrBadDescription, sv.Name)
|
2022-05-25 17:27:18 +00:00
|
|
|
}
|
2022-06-06 17:00:39 +00:00
|
|
|
stateVariables[sv.Name] = sv
|
2022-05-25 17:27:18 +00:00
|
|
|
}
|
2022-06-06 17:00:39 +00:00
|
|
|
actions := scpd.ActionByName
|
2022-05-25 17:27:18 +00:00
|
|
|
for _, xmlAction := range xmlDesc.Actions {
|
2022-06-06 17:00:39 +00:00
|
|
|
action, err := actionFromXML(xmlAction, scpd)
|
2022-05-25 17:27:18 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("processing action %q: %w", xmlAction.Name, err)
|
|
|
|
}
|
2022-06-06 17:00:39 +00:00
|
|
|
if _, exists := actions[action.Name]; exists {
|
2022-05-25 17:27:18 +00:00
|
|
|
return nil, fmt.Errorf("%w: multiple actions with name %q",
|
2023-03-09 17:08:45 +00:00
|
|
|
ErrBadDescription, action.Name)
|
2022-05-25 17:27:18 +00:00
|
|
|
}
|
2022-06-06 17:00:39 +00:00
|
|
|
actions[action.Name] = action
|
2022-05-25 17:27:18 +00:00
|
|
|
}
|
2022-06-06 17:00:39 +00:00
|
|
|
return scpd, nil
|
2022-05-25 17:27:18 +00:00
|
|
|
}
|
|
|
|
|
2022-06-06 17:00:39 +00:00
|
|
|
// SortedActions returns the actions, in order of name.
|
|
|
|
func (scpd *SCPD) SortedActions() []*Action {
|
|
|
|
actions := make([]*Action, 0, len(scpd.ActionByName))
|
|
|
|
for _, a := range scpd.ActionByName {
|
|
|
|
actions = append(actions, a)
|
2022-05-25 17:27:18 +00:00
|
|
|
}
|
2022-06-06 17:00:39 +00:00
|
|
|
sort.Slice(actions, func(i, j int) bool {
|
|
|
|
return actions[i].Name < actions[j].Name
|
|
|
|
})
|
|
|
|
return actions
|
2022-05-25 17:27:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Action describes a single UPnP SOAP action.
|
|
|
|
type Action struct {
|
2022-06-06 17:00:39 +00:00
|
|
|
SCPD *SCPD
|
|
|
|
Name string
|
|
|
|
InArgs []*Argument
|
|
|
|
OutArgs []*Argument
|
2022-05-25 17:27:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// actionFromXML creates an Action from the given XML description.
|
2022-06-06 17:00:39 +00:00
|
|
|
func actionFromXML(xmlAction *xmlsrvdesc.Action, scpd *SCPD) (*Action, error) {
|
2022-05-25 17:27:18 +00:00
|
|
|
if xmlAction.Name == "" {
|
2023-03-09 17:08:45 +00:00
|
|
|
return nil, fmt.Errorf("%w: empty action name", ErrBadDescription)
|
2022-05-25 17:27:18 +00:00
|
|
|
}
|
2022-06-06 17:00:39 +00:00
|
|
|
action := &Action{
|
|
|
|
SCPD: scpd,
|
|
|
|
Name: xmlAction.Name,
|
|
|
|
}
|
2022-05-25 17:27:18 +00:00
|
|
|
var inArgs []*Argument
|
|
|
|
var outArgs []*Argument
|
|
|
|
for _, xmlArg := range xmlAction.Arguments {
|
2022-06-06 17:00:39 +00:00
|
|
|
arg, err := argumentFromXML(xmlArg, action)
|
2022-05-25 17:27:18 +00:00
|
|
|
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",
|
2023-03-09 17:08:45 +00:00
|
|
|
ErrBadDescription, xmlArg.Name, xmlArg.Direction)
|
2022-05-25 17:27:18 +00:00
|
|
|
}
|
|
|
|
}
|
2022-06-06 17:00:39 +00:00
|
|
|
action.InArgs = inArgs
|
|
|
|
action.OutArgs = outArgs
|
|
|
|
return action, nil
|
2022-05-25 17:27:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Argument description data.
|
|
|
|
type Argument struct {
|
2022-06-06 17:00:39 +00:00
|
|
|
Action *Action
|
|
|
|
Name string
|
|
|
|
RelatedStateVariableName string
|
2022-05-25 17:27:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// argumentFromXML creates an Argument from the XML description.
|
2022-06-06 17:00:39 +00:00
|
|
|
func argumentFromXML(xmlArg *xmlsrvdesc.Argument, action *Action) (*Argument, error) {
|
2022-05-25 17:27:18 +00:00
|
|
|
if xmlArg.Name == "" {
|
2023-03-09 17:08:45 +00:00
|
|
|
return nil, fmt.Errorf("%w: empty argument name", ErrBadDescription)
|
2022-05-25 17:27:18 +00:00
|
|
|
}
|
|
|
|
if xmlArg.RelatedStateVariable == "" {
|
2023-03-09 17:08:45 +00:00
|
|
|
return nil, fmt.Errorf("%w: empty related state variable", ErrBadDescription)
|
2022-05-25 17:27:18 +00:00
|
|
|
}
|
|
|
|
return &Argument{
|
2022-06-06 17:00:39 +00:00
|
|
|
Action: action,
|
|
|
|
Name: xmlArg.Name,
|
|
|
|
RelatedStateVariableName: xmlArg.RelatedStateVariable,
|
2022-05-25 17:27:18 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2022-06-06 17:00:39 +00:00
|
|
|
func (arg *Argument) RelatedStateVariable() (*StateVariable, error) {
|
|
|
|
if v, ok := arg.Action.SCPD.VariableByName[arg.RelatedStateVariableName]; ok {
|
|
|
|
return v, nil
|
|
|
|
}
|
2023-03-09 17:08:45 +00:00
|
|
|
return nil, fmt.Errorf("%w: state variable %q", ErrMissingDefinition, arg.RelatedStateVariableName)
|
2022-05-25 17:27:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// StateVariable description data.
|
|
|
|
type StateVariable struct {
|
2022-06-06 17:00:39 +00:00
|
|
|
Name string
|
|
|
|
DataType string
|
2023-03-09 18:23:18 +00:00
|
|
|
|
|
|
|
AllowedValues []string
|
2022-05-25 17:27:18 +00:00
|
|
|
}
|
|
|
|
|
2022-05-27 06:00:09 +00:00
|
|
|
func stateVariableFromXML(xmlSV *xmlsrvdesc.StateVariable) (*StateVariable, error) {
|
2022-05-25 17:27:18 +00:00
|
|
|
if xmlSV.Name == "" {
|
2023-03-09 17:08:45 +00:00
|
|
|
return nil, fmt.Errorf("%w: empty state variable name", ErrBadDescription)
|
2022-05-25 17:27:18 +00:00
|
|
|
}
|
|
|
|
if xmlSV.DataType.Type != "" {
|
|
|
|
return nil, fmt.Errorf("%w: unsupported data type %q",
|
2023-03-09 17:08:45 +00:00
|
|
|
ErrUnsupportedDescription, xmlSV.DataType.Type)
|
2022-05-25 17:27:18 +00:00
|
|
|
}
|
2023-03-09 18:23:18 +00:00
|
|
|
if xmlSV.DataType.Name != "string" && len(xmlSV.AllowedValues) > 0 {
|
|
|
|
return nil, fmt.Errorf("%w: allowedValueList is currently unsupported for type %q",
|
|
|
|
ErrUnsupportedDescription, xmlSV.DataType.Name)
|
|
|
|
}
|
2022-05-25 17:27:18 +00:00
|
|
|
return &StateVariable{
|
2023-03-09 18:23:18 +00:00
|
|
|
Name: xmlSV.Name,
|
|
|
|
DataType: xmlSV.DataType.Name,
|
|
|
|
AllowedValues: xmlSV.AllowedValues,
|
2022-05-25 17:27:18 +00:00
|
|
|
}, nil
|
|
|
|
}
|