2018-10-13 14:04:17 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"archive/zip"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"os"
|
2022-03-13 09:02:29 +00:00
|
|
|
"sort"
|
2018-10-13 14:04:17 +00:00
|
|
|
"strings"
|
2021-08-14 09:17:22 +00:00
|
|
|
"text/template"
|
2018-10-13 14:04:17 +00:00
|
|
|
|
2022-11-01 19:02:13 +00:00
|
|
|
"git.cyrilix.bzh/cyrilix/goupnp"
|
|
|
|
"git.cyrilix.bzh/cyrilix/goupnp/scpd"
|
|
|
|
"git.cyrilix.bzh/cyrilix/goupnp/soap"
|
2018-10-13 14:04:17 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// DCP collects together information about a UPnP Device Control Protocol.
|
|
|
|
type DCP struct {
|
|
|
|
Metadata DCPMetadata
|
2022-03-21 13:46:32 +00:00
|
|
|
DocURLs []string
|
2018-10-13 14:04:17 +00:00
|
|
|
DeviceTypes map[string]*URNParts
|
|
|
|
ServiceTypes map[string]*URNParts
|
|
|
|
Services []SCPDWithURN
|
|
|
|
}
|
|
|
|
|
|
|
|
func newDCP(metadata DCPMetadata) *DCP {
|
|
|
|
return &DCP{
|
|
|
|
Metadata: metadata,
|
|
|
|
DeviceTypes: make(map[string]*URNParts),
|
|
|
|
ServiceTypes: make(map[string]*URNParts),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-21 13:46:32 +00:00
|
|
|
func (dcp *DCP) Reset() {
|
|
|
|
dcp.DocURLs = nil
|
|
|
|
dcp.DeviceTypes = make(map[string]*URNParts)
|
|
|
|
dcp.ServiceTypes = make(map[string]*URNParts)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (dcp *DCP) processZipFile(archive []*zip.File, devices, services []string) error {
|
|
|
|
var f int
|
|
|
|
for _, devicesGlob := range devices {
|
|
|
|
for _, deviceFile := range globFiles(devicesGlob, archive) {
|
|
|
|
if err := dcp.processDeviceFile(deviceFile); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
f++
|
2018-10-13 14:04:17 +00:00
|
|
|
}
|
|
|
|
}
|
2022-03-21 13:46:32 +00:00
|
|
|
for _, scpdsGlob := range services {
|
|
|
|
for _, scpdFile := range globFiles(scpdsGlob, archive) {
|
|
|
|
if err := dcp.processSCPDFile(scpdFile); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
f++
|
2018-10-13 14:04:17 +00:00
|
|
|
}
|
|
|
|
}
|
2022-03-21 13:46:32 +00:00
|
|
|
if f < 1 {
|
|
|
|
return fmt.Errorf("no sdcp/device found in %q and %q", devices, services)
|
|
|
|
}
|
2018-10-13 14:04:17 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (dcp *DCP) processDeviceFile(file *zip.File) error {
|
|
|
|
var device goupnp.Device
|
|
|
|
if err := unmarshalXmlFile(file, &device); err != nil {
|
|
|
|
return fmt.Errorf("error decoding device XML from file %q: %v", file.Name, err)
|
|
|
|
}
|
|
|
|
var mainErr error
|
|
|
|
device.VisitDevices(func(d *goupnp.Device) {
|
|
|
|
t := strings.TrimSpace(d.DeviceType)
|
|
|
|
if t != "" {
|
|
|
|
u, err := extractURNParts(t, deviceURNPrefix)
|
|
|
|
if err != nil {
|
|
|
|
mainErr = err
|
|
|
|
}
|
|
|
|
dcp.DeviceTypes[t] = u
|
|
|
|
}
|
|
|
|
})
|
|
|
|
device.VisitServices(func(s *goupnp.Service) {
|
2022-11-01 19:03:30 +00:00
|
|
|
u, err := extractURNParts(s.ServiceType, dcp.Metadata.ServiceURNPrefix)
|
2018-10-13 14:04:17 +00:00
|
|
|
if err != nil {
|
|
|
|
mainErr = err
|
|
|
|
}
|
|
|
|
dcp.ServiceTypes[s.ServiceType] = u
|
|
|
|
})
|
|
|
|
return mainErr
|
|
|
|
}
|
|
|
|
|
2021-08-14 09:17:22 +00:00
|
|
|
func (dcp *DCP) writeCode(outFile string, codeTmpl *template.Template) error {
|
2018-10-13 14:04:17 +00:00
|
|
|
packageFile, err := os.Create(outFile)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
var output io.WriteCloser = packageFile
|
2021-08-14 09:17:22 +00:00
|
|
|
if err = codeTmpl.Execute(output, dcp); err != nil {
|
2018-10-13 14:04:17 +00:00
|
|
|
output.Close()
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return output.Close()
|
|
|
|
}
|
|
|
|
|
2022-03-13 09:02:29 +00:00
|
|
|
func (dcp *DCP) OrderedServices() []SCPDWithURN {
|
|
|
|
services := append([]SCPDWithURN{}, dcp.Services...)
|
|
|
|
sort.SliceStable(services, func(i, j int) bool {
|
|
|
|
return services[i].URNParts.URN < services[j].URNParts.URN
|
|
|
|
})
|
|
|
|
return services
|
|
|
|
}
|
|
|
|
|
|
|
|
func orderedURNParts(urnMap map[string]*URNParts) []*URNParts {
|
|
|
|
urns := make([]*URNParts, 0, len(urnMap))
|
|
|
|
for _, urn := range urnMap {
|
|
|
|
urns = append(urns, urn)
|
|
|
|
}
|
|
|
|
sort.SliceStable(urns, func(i, j int) bool {
|
|
|
|
return urns[i].URN < urns[j].URN
|
|
|
|
})
|
|
|
|
return urns
|
|
|
|
}
|
|
|
|
|
|
|
|
func (dcp *DCP) OrderedDeviceTypes() []*URNParts {
|
|
|
|
return orderedURNParts(dcp.DeviceTypes)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (dcp *DCP) OrderedServiceTypes() []*URNParts {
|
|
|
|
return orderedURNParts(dcp.ServiceTypes)
|
|
|
|
}
|
|
|
|
|
2018-10-13 14:04:17 +00:00
|
|
|
func (dcp *DCP) processSCPDFile(file *zip.File) error {
|
|
|
|
scpd := new(scpd.SCPD)
|
|
|
|
if err := unmarshalXmlFile(file, scpd); err != nil {
|
|
|
|
return fmt.Errorf("error decoding SCPD XML from file %q: %v", file.Name, err)
|
|
|
|
}
|
|
|
|
scpd.Clean()
|
2022-11-01 19:03:30 +00:00
|
|
|
urnParts, err := urnPartsFromSCPDFilename(file.Name, dcp.Metadata.ServiceURNPrefix)
|
2018-10-13 14:04:17 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not recognize SCPD filename %q: %v", file.Name, err)
|
|
|
|
}
|
|
|
|
dcp.Services = append(dcp.Services, SCPDWithURN{
|
|
|
|
URNParts: urnParts,
|
|
|
|
SCPD: scpd,
|
|
|
|
})
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type SCPDWithURN struct {
|
|
|
|
*URNParts
|
|
|
|
SCPD *scpd.SCPD
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *SCPDWithURN) WrapArguments(args []*scpd.Argument) (argumentWrapperList, error) {
|
|
|
|
wrappedArgs := make(argumentWrapperList, len(args))
|
|
|
|
for i, arg := range args {
|
|
|
|
wa, err := s.wrapArgument(arg)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
wrappedArgs[i] = wa
|
|
|
|
}
|
|
|
|
return wrappedArgs, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *SCPDWithURN) wrapArgument(arg *scpd.Argument) (*argumentWrapper, error) {
|
|
|
|
relVar := s.SCPD.GetStateVariable(arg.RelatedStateVariable)
|
|
|
|
if relVar == nil {
|
|
|
|
return nil, fmt.Errorf("no such state variable: %q, for argument %q", arg.RelatedStateVariable, arg.Name)
|
|
|
|
}
|
2021-08-18 16:03:52 +00:00
|
|
|
cnv, ok := soap.TypeDataMap[relVar.DataType.Name]
|
2018-10-13 14:04:17 +00:00
|
|
|
if !ok {
|
|
|
|
return nil, fmt.Errorf("unknown data type: %q, for state variable %q, for argument %q", relVar.DataType.Type, arg.RelatedStateVariable, arg.Name)
|
|
|
|
}
|
|
|
|
return &argumentWrapper{
|
2021-08-18 16:03:52 +00:00
|
|
|
Argument: *arg,
|
|
|
|
relVar: relVar,
|
|
|
|
typeDataV1: cnv,
|
2018-10-13 14:04:17 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type argumentWrapper struct {
|
|
|
|
scpd.Argument
|
2021-08-18 16:03:52 +00:00
|
|
|
relVar *scpd.StateVariable
|
|
|
|
typeDataV1 soap.TypeData
|
2018-10-13 14:04:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (arg *argumentWrapper) AsParameter() string {
|
2021-08-18 16:03:52 +00:00
|
|
|
return fmt.Sprintf("%s %s", arg.Name, arg.typeDataV1.GoTypeName())
|
2018-10-13 14:04:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (arg *argumentWrapper) HasDoc() bool {
|
|
|
|
rng := arg.relVar.AllowedValueRange
|
|
|
|
return ((rng != nil && (rng.Minimum != "" || rng.Maximum != "" || rng.Step != "")) ||
|
|
|
|
len(arg.relVar.AllowedValues) > 0)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (arg *argumentWrapper) Document() string {
|
|
|
|
relVar := arg.relVar
|
|
|
|
if rng := relVar.AllowedValueRange; rng != nil {
|
|
|
|
var parts []string
|
|
|
|
if rng.Minimum != "" {
|
|
|
|
parts = append(parts, fmt.Sprintf("minimum=%s", rng.Minimum))
|
|
|
|
}
|
|
|
|
if rng.Maximum != "" {
|
|
|
|
parts = append(parts, fmt.Sprintf("maximum=%s", rng.Maximum))
|
|
|
|
}
|
|
|
|
if rng.Step != "" {
|
|
|
|
parts = append(parts, fmt.Sprintf("step=%s", rng.Step))
|
|
|
|
}
|
|
|
|
return "allowed value range: " + strings.Join(parts, ", ")
|
|
|
|
}
|
|
|
|
if len(relVar.AllowedValues) != 0 {
|
|
|
|
return "allowed values: " + strings.Join(relVar.AllowedValues, ", ")
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2021-08-18 16:03:52 +00:00
|
|
|
func (arg *argumentWrapper) MarshalV1() string {
|
|
|
|
return fmt.Sprintf("soap.%s(%s)", arg.typeDataV1.MarshalFunc(), arg.Name)
|
2018-10-13 14:04:17 +00:00
|
|
|
}
|
|
|
|
|
2021-08-18 16:03:52 +00:00
|
|
|
func (arg *argumentWrapper) UnmarshalV1(objVar string) string {
|
|
|
|
return fmt.Sprintf("soap.%s(%s.%s)", arg.typeDataV1.UnmarshalFunc(), objVar, arg.Name)
|
2018-10-13 14:04:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type argumentWrapperList []*argumentWrapper
|
|
|
|
|
|
|
|
func (args argumentWrapperList) HasDoc() bool {
|
|
|
|
for _, arg := range args {
|
|
|
|
if arg.HasDoc() {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
type URNParts struct {
|
|
|
|
URN string
|
|
|
|
Name string
|
|
|
|
Version string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *URNParts) Const() string {
|
|
|
|
return fmt.Sprintf("URN_%s_%s", u.Name, u.Version)
|
|
|
|
}
|
|
|
|
|
|
|
|
// extractURNParts extracts the name and version from a URN string.
|
|
|
|
func extractURNParts(urn, expectedPrefix string) (*URNParts, error) {
|
|
|
|
if !strings.HasPrefix(urn, expectedPrefix) {
|
|
|
|
return nil, fmt.Errorf("%q does not have expected prefix %q", urn, expectedPrefix)
|
|
|
|
}
|
|
|
|
parts := strings.SplitN(strings.TrimPrefix(urn, expectedPrefix), ":", 2)
|
|
|
|
if len(parts) != 2 {
|
|
|
|
return nil, fmt.Errorf("%q does not have a name and version", urn)
|
|
|
|
}
|
|
|
|
name, version := parts[0], parts[1]
|
|
|
|
return &URNParts{urn, name, version}, nil
|
|
|
|
}
|