diff --git a/cmd/specgen/pkgtmpl.go b/cmd/specgen/pkgtmpl.go new file mode 100644 index 0000000..4ae708f --- /dev/null +++ b/cmd/specgen/pkgtmpl.go @@ -0,0 +1,66 @@ +package main + +import ( + "text/template" +) + +var packageTmpl = template.Must(template.New("package").Parse(`package {{.Name}} + +import "github.com/huin/goupnp/soap" + +const ({{range .DeviceTypes}} + {{.Const}} = "{{.URN}}" +{{end}}) + +const ({{range .ServiceTypes}} + {{.Const}} = "{{.URN}}" +{{end}}) + +{{range .Services}} +{{$srv := .}} +{{$srvIdent := printf "%s%s" .Name .Version}} + +// {{$srvIdent}} is a client for UPnP SOAP service with URN "{{.URN}}". +type {{$srvIdent}} struct { + SOAPClient soap.SOAPClient +} + +{{range .SCPD.Actions}}{{/* loops over *SCPDWithURN values */}} + +{{$reqType := printf "_%s_%s_Request" $srvIdent .Name}} +{{$respType := printf "_%s_%s_Response" $srvIdent .Name}} + +// {{$reqType}} is the XML structure for the input arguments for action {{.Name}}. +type {{$reqType}} struct {{"{"}}{{range .Arguments}}{{if .IsInput}} + {{.Name}} string +{{end}}{{end}}} + +// {{$respType}} is the XML structure for the output arguments for action {{.Name}}. +type {{$respType}} struct {{"{"}}{{range .Arguments}}{{if .IsOutput}} + {{.Name}} string +{{end}}{{end}}} + +func (client *{{$srvIdent}}) {{.Name}}({{range .Arguments}}{{if .IsInput}} + {{.Name}} string, +{{end}}{{end}}) ({{range .Arguments}}{{if .IsOutput}} + {{.Name}} string, +{{end}}{{end}} err error) { + request := {{$reqType}}{ +{{range .Arguments}}{{if .IsInput}} + {{.Name}}: {{.Name}}, +{{end}}{{end}} + } + var response {{$respType}} + err = client.SOAPClient.PerformAction({{$srv.URNParts.Const}}, {{.Name}}, &request, &response) + if err != nil { + return + } +{{range .Arguments}}{{if .IsOutput}} + {{.Name}} = response.{{.Name}} +{{end}}{{end}} + return +} + +{{end}}{{/* range .SCPD.Actions */}} +{{end}}{{/* range .Services */}} +`)) diff --git a/cmd/specgen/specgen.go b/cmd/specgen/specgen.go new file mode 100644 index 0000000..1ee795c --- /dev/null +++ b/cmd/specgen/specgen.go @@ -0,0 +1,303 @@ +// specgen generates Go code from the UPnP specification files. +// +// The specification is available for download from: +// http://upnp.org/resources/upnpresources.zip +package main + +import ( + "archive/zip" + "bytes" + "encoding/xml" + "flag" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "path" + "path/filepath" + "regexp" + "strings" + + "github.com/huin/goupnp" + "github.com/huin/goupnp/scpd" +) + +// flags +var ( + specFilename = flag.String("spec", "", "Path to the specification file.") + outDir = flag.String("out-dir", "", "Path to the output directory.") +) + +var ( + deviceURNPrefix = "urn:schemas-upnp-org:device:" + serviceURNPrefix = "urn:schemas-upnp-org:service:" +) + +func main() { + flag.Parse() + + if *specFilename == "" { + log.Fatal("--spec is required") + } + if *outDir == "" { + log.Fatal("--out-dir is required") + } + + specArchive, err := openZipfile(*specFilename) + if err != nil { + log.Fatalf("Error opening %s: %v", *specFilename) + } + defer specArchive.Close() + + dcpsCol := newDcpsCollection(map[string]string{ + "Internet Gateway_1": "internetgateway1", + "Internet Gateway_2": "internetgateway2", + }) + for _, f := range globFiles("standardizeddcps/*/*.zip", specArchive.Reader) { + dirName := strings.TrimPrefix(f.Name, "standardizeddcps/") + slashIndex := strings.Index(dirName, "/") + if slashIndex == -1 { + // Should not happen. + log.Printf("Could not find / in %q", dirName) + return + } + dirName = dirName[:slashIndex] + + dcp := dcpsCol.dcpsForDir(dirName) + if dcp == nil { + log.Printf("No alias defined for directory %q: skipping", dirName) + continue + } + + dcp.processZipFile(f) + } + + for _, dcp := range dcpsCol.dcpsByAlias { + if err := dcp.writePackage(*outDir); err != nil { + log.Printf("Error writing package %q: %v", dcp.Name, err) + } + } +} + +type dcpsCollection struct { + dcpsAliasByDir map[string]string + dcpsByAlias map[string]*DCP +} + +func newDcpsCollection(dcpsAliasByDir map[string]string) *dcpsCollection { + c := &dcpsCollection{ + dcpsAliasByDir: dcpsAliasByDir, + dcpsByAlias: make(map[string]*DCP), + } + for _, alias := range dcpsAliasByDir { + c.dcpsByAlias[alias] = newDCP(alias) + } + return c +} + +func (c dcpsCollection) dcpsForDir(dirName string) *DCP { + alias, ok := c.dcpsAliasByDir[dirName] + if !ok { + return nil + } + return c.dcpsByAlias[alias] +} + +// DCP collects together information about a UPnP Device Control Protocol. +type DCP struct { + Name string + DeviceTypes map[string]*URNParts + ServiceTypes map[string]*URNParts + Services []SCPDWithURN +} + +func newDCP(name string) *DCP { + return &DCP{ + Name: name, + DeviceTypes: make(map[string]*URNParts), + ServiceTypes: make(map[string]*URNParts), + } +} + +func (dcp *DCP) processZipFile(file *zip.File) { + archive, err := openChildZip(file) + if err != nil { + log.Println("Error reading child zip file:", err) + return + } + for _, deviceFile := range globFiles("*/device/*.xml", archive) { + dcp.processDeviceFile(deviceFile) + } + for _, scpdFile := range globFiles("*/service/*.xml", archive) { + dcp.processSCPDFile(scpdFile) + } +} + +func (dcp *DCP) processDeviceFile(file *zip.File) { + var device goupnp.Device + if err := unmarshalXmlFile(file, &device); err != nil { + log.Printf("Error decoding device XML from file %q: %v", file.Name, err) + return + } + device.VisitDevices(func(d *goupnp.Device) { + t := strings.TrimSpace(d.DeviceType) + if t != "" { + u, err := extractURNParts(t, deviceURNPrefix) + if err != nil { + log.Println(err) + return + } + dcp.DeviceTypes[t] = u + } + }) + device.VisitServices(func(s *goupnp.Service) { + u, err := extractURNParts(s.ServiceType, serviceURNPrefix) + if err != nil { + log.Println(err) + return + } + dcp.ServiceTypes[s.ServiceType] = u + }) +} + +func (dcp *DCP) writePackage(outDir string) error { + packageDirname := filepath.Join(outDir, dcp.Name) + err := os.MkdirAll(packageDirname, os.ModePerm) + if err != nil && !os.IsExist(err) { + return err + } + packageFilename := filepath.Join(packageDirname, dcp.Name+".go") + packageFile, err := os.Create(packageFilename) + if err != nil { + return err + } + defer packageFile.Close() + return packageTmpl.Execute(packageFile, dcp) +} + +func (dcp *DCP) processSCPDFile(file *zip.File) { + scpd := new(scpd.SCPD) + if err := unmarshalXmlFile(file, scpd); err != nil { + log.Printf("Error decoding SCPD XML from file %q: %v", file.Name, err) + return + } + scpd.Clean() + urnParts, err := urnPartsFromSCPDFilename(file.Name) + if err != nil { + log.Printf("Could not recognize SCPD filename %q: %v", file.Name, err) + return + } + dcp.Services = append(dcp.Services, SCPDWithURN{ + URNParts: urnParts, + SCPD: scpd, + }) +} + +type SCPDWithURN struct { + *URNParts + SCPD *scpd.SCPD +} + +type closeableZipReader struct { + io.Closer + *zip.Reader +} + +func openZipfile(filename string) (*closeableZipReader, error) { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + fi, err := file.Stat() + if err != nil { + return nil, err + } + archive, err := zip.NewReader(file, fi.Size()) + if err != nil { + return nil, err + } + return &closeableZipReader{ + Closer: file, + Reader: archive, + }, nil +} + +// openChildZip opens a zip file within another zip file. +func openChildZip(file *zip.File) (*zip.Reader, error) { + zipFile, err := file.Open() + if err != nil { + return nil, err + } + defer zipFile.Close() + + zipBytes, err := ioutil.ReadAll(zipFile) + if err != nil { + return nil, err + } + + return zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes))) +} + +func globFiles(pattern string, archive *zip.Reader) []*zip.File { + var files []*zip.File + for _, f := range archive.File { + if matched, err := path.Match(pattern, f.Name); err != nil { + // This shouldn't happen - all patterns are hard-coded, errors in them + // are a programming error. + panic(err) + } else if matched { + files = append(files, f) + } + } + return files +} + +func unmarshalXmlFile(file *zip.File, data interface{}) error { + r, err := file.Open() + if err != nil { + return err + } + decoder := xml.NewDecoder(r) + r.Close() + return decoder.Decode(data) +} + +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 +} + +var scpdFilenameRe = regexp.MustCompile( + `.*/([a-zA-Z0-9]+)([0-9]+)\.xml`) + +func urnPartsFromSCPDFilename(filename string) (*URNParts, error) { + parts := scpdFilenameRe.FindStringSubmatch(filename) + if len(parts) != 3 { + return nil, fmt.Errorf("SCPD filename %q does not have expected number of parts", filename) + } + name, version := parts[1], parts[2] + return &URNParts{ + URN: serviceURNPrefix + name + ":" + version, + Name: name, + Version: version, + }, nil +}