add support for Open Connectivity Foundation DCPs
This commit is contained in:
parent
e739c716b4
commit
9af4afce08
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
*.zip
|
||||
*.sublime-workspace
|
||||
*.download
|
2
Makefile
Normal file
2
Makefile
Normal file
@ -0,0 +1,2 @@
|
||||
gen:
|
||||
(cd cmd/goupnpdcpgen/; go install); go generate ./...
|
@ -2,15 +2,21 @@ package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var packageTmpl = template.Must(template.New("package").Parse(`{{$name := .Metadata.Name}}
|
||||
var templateFuncs = template.FuncMap{
|
||||
"base": filepath.Base,
|
||||
}
|
||||
|
||||
var packageTmpl = template.Must(template.New("package").Funcs(templateFuncs).Parse(`{{$name := .Metadata.Name}}
|
||||
// Client for UPnP Device Control Protocol {{.Metadata.OfficialName}}.
|
||||
// {{if .Metadata.DocURL}}
|
||||
// This DCP is documented in detail at: {{.Metadata.DocURL}}{{end}}
|
||||
// {{if .DocURLs}}
|
||||
// This DCP is documented in detail at: {{range .DocURLs}}
|
||||
// - {{.}}{{end}}{{end}}
|
||||
//
|
||||
// Typically, use one of the New* functions to create clients for services.
|
||||
package {{$name}}
|
||||
package {{$name | base}}
|
||||
|
||||
// ***********************************************************
|
||||
// GENERATED FILE - DO NOT EDIT BY HAND. See README.md
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
// DCP collects together information about a UPnP Device Control Protocol.
|
||||
type DCP struct {
|
||||
Metadata DCPMetadata
|
||||
DocURLs []string
|
||||
DeviceTypes map[string]*URNParts
|
||||
ServiceTypes map[string]*URNParts
|
||||
Services []SCPDWithURN
|
||||
@ -29,22 +30,33 @@ func newDCP(metadata DCPMetadata) *DCP {
|
||||
}
|
||||
}
|
||||
|
||||
func (dcp *DCP) processZipFile(filename string) error {
|
||||
archive, err := zip.OpenReader(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading zip file %q: %v", filename, err)
|
||||
}
|
||||
defer archive.Close()
|
||||
for _, deviceFile := range globFiles("*/device/*.xml", archive) {
|
||||
if err := dcp.processDeviceFile(deviceFile); err != nil {
|
||||
return err
|
||||
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++
|
||||
}
|
||||
}
|
||||
for _, scpdFile := range globFiles("*/service/*.xml", archive) {
|
||||
if err := dcp.processSCPDFile(scpdFile); err != nil {
|
||||
return err
|
||||
for _, scpdsGlob := range services {
|
||||
for _, scpdFile := range globFiles(scpdsGlob, archive) {
|
||||
if err := dcp.processSCPDFile(scpdFile); err != nil {
|
||||
return err
|
||||
}
|
||||
f++
|
||||
}
|
||||
}
|
||||
if f < 1 {
|
||||
return fmt.Errorf("no sdcp/device found in %q and %q", devices, services)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -9,24 +9,22 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func acquireFile(specFilename string, xmlSpecURL string) error {
|
||||
if f, err := os.Open(specFilename); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
f.Close()
|
||||
tmpFilename := specFilename + ".download"
|
||||
defer os.Remove(tmpFilename)
|
||||
|
||||
if fileExists(specFilename) {
|
||||
return nil
|
||||
}
|
||||
|
||||
tmpFilename := specFilename + ".download"
|
||||
if err := downloadFile(tmpFilename, xmlSpecURL); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.Rename(tmpFilename, specFilename)
|
||||
return copyFile(specFilename, tmpFilename)
|
||||
}
|
||||
|
||||
func downloadFile(filename, url string) error {
|
||||
@ -54,10 +52,11 @@ func downloadFile(filename, url string) error {
|
||||
return w.Close()
|
||||
}
|
||||
|
||||
func globFiles(pattern string, archive *zip.ReadCloser) []*zip.File {
|
||||
func globFiles(pattern string, archive []*zip.File) []*zip.File {
|
||||
var files []*zip.File
|
||||
for _, f := range archive.File {
|
||||
if matched, err := path.Match(pattern, f.Name); err != nil {
|
||||
pattern = strings.ToLower(pattern)
|
||||
for _, f := range archive {
|
||||
if matched, err := path.Match(pattern, strings.ToLower(f.Name)); err != nil {
|
||||
// This shouldn't happen - all patterns are hard-coded, errors in them
|
||||
// are a programming error.
|
||||
panic(err)
|
||||
@ -93,3 +92,30 @@ func urnPartsFromSCPDFilename(filename string) (*URNParts, error) {
|
||||
Version: version,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func copyFile(dst string, src string) error {
|
||||
f, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeFile(dst, f)
|
||||
}
|
||||
|
||||
func writeFile(dst string, r io.ReadCloser) error {
|
||||
defer r.Close()
|
||||
f, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(f, r)
|
||||
return err
|
||||
}
|
||||
|
||||
func fileExists(p string) bool {
|
||||
f, err := os.Open(p)
|
||||
if err != nil {
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
f.Close()
|
||||
return true
|
||||
}
|
||||
|
@ -35,25 +35,19 @@ func run(dcpName, specsDir string, useGofmt bool) error {
|
||||
return fmt.Errorf("could not create specs-dir %q: %v", specsDir, err)
|
||||
}
|
||||
|
||||
for _, d := range dcpMetadata {
|
||||
if d.Name != dcpName {
|
||||
for _, metadata := range dcpMetadata {
|
||||
if metadata.Name != dcpName {
|
||||
continue
|
||||
}
|
||||
specFilename := filepath.Join(specsDir, d.Name+".zip")
|
||||
err := acquireFile(specFilename, d.XMLSpecURL)
|
||||
|
||||
dcp := newDCP(metadata)
|
||||
|
||||
err := metadata.Src.process(".", metadata.Name, dcp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not acquire spec for %s: %v", d.Name, err)
|
||||
return fmt.Errorf("error processing spec %s: %v", metadata.Name, err)
|
||||
}
|
||||
dcp := newDCP(d)
|
||||
if err := dcp.processZipFile(specFilename); err != nil {
|
||||
return fmt.Errorf("error processing spec for %s in file %q: %v", d.Name, specFilename, err)
|
||||
}
|
||||
for i, hack := range d.Hacks {
|
||||
if err := hack(dcp); err != nil {
|
||||
return fmt.Errorf("error with Hack[%d] for %s: %v", i, d.Name, err)
|
||||
}
|
||||
}
|
||||
if err := dcp.writeCode(d.Name+".go", useGofmt); err != nil {
|
||||
|
||||
if err := dcp.writeCode(filepath.Base(metadata.Name)+".go", useGofmt); err != nil {
|
||||
return fmt.Errorf("error writing package %q: %v", dcp.Metadata.Name, err)
|
||||
}
|
||||
|
||||
|
@ -1,69 +1,127 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DCP contains extra metadata to use when generating DCP source files.
|
||||
type DCPMetadata struct {
|
||||
Name string // What to name the Go DCP package.
|
||||
OfficialName string // Official name for the DCP.
|
||||
DocURL string // Optional - URL for further documentation about the DCP.
|
||||
XMLSpecURL string // Where to download the XML spec from.
|
||||
// Any special-case functions to run against the DCP before writing it out.
|
||||
Hacks []DCPHackFn
|
||||
Src dcpProvider
|
||||
}
|
||||
|
||||
var dcpMetadata = []DCPMetadata{
|
||||
{
|
||||
Name: "internetgateway1",
|
||||
OfficialName: "Internet Gateway Device v1",
|
||||
DocURL: "http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v1-Device.pdf",
|
||||
XMLSpecURL: "http://upnp.org/specs/gw/UPnP-gw-IGD-TestFiles-20010921.zip",
|
||||
Hacks: []DCPHackFn{totalBytesHack},
|
||||
Src: upnpdotorg{
|
||||
DocURL: "http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v1-Device.pdf",
|
||||
XMLSpecURL: "http://upnp.org/specs/gw/UPnP-gw-IGD-TestFiles-20010921.zip",
|
||||
Hacks: []DCPHackFn{
|
||||
fixTotalBytes("urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "internetgateway2",
|
||||
OfficialName: "Internet Gateway Device v2",
|
||||
DocURL: "http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v2-Device.pdf",
|
||||
XMLSpecURL: "http://upnp.org/specs/gw/UPnP-gw-IGD-Testfiles-20110224.zip",
|
||||
Hacks: []DCPHackFn{
|
||||
func(dcp *DCP) error {
|
||||
missingURN := "urn:schemas-upnp-org:service:WANIPv6FirewallControl:1"
|
||||
if _, ok := dcp.ServiceTypes[missingURN]; ok {
|
||||
return nil
|
||||
}
|
||||
urnParts, err := extractURNParts(missingURN, serviceURNPrefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dcp.ServiceTypes[missingURN] = urnParts
|
||||
return nil
|
||||
}, totalBytesHack,
|
||||
Src: upnpdotorg{
|
||||
DocURL: "http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v2-Device.pdf",
|
||||
XMLSpecURL: "http://upnp.org/specs/gw/UPnP-gw-IGD-Testfiles-20110224.zip",
|
||||
Hacks: []DCPHackFn{
|
||||
fixMissingURN("urn:schemas-upnp-org:service:WANIPv6FirewallControl:1"),
|
||||
fixTotalBytes("urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "av1",
|
||||
OfficialName: "MediaServer v1 and MediaRenderer v1",
|
||||
DocURL: "http://upnp.org/specs/av/av1/",
|
||||
XMLSpecURL: "http://upnp.org/specs/av/UPnP-av-TestFiles-20070927.zip",
|
||||
Src: upnpdotorg{
|
||||
DocURL: "http://upnp.org/specs/av/av1/",
|
||||
XMLSpecURL: "http://upnp.org/specs/av/UPnP-av-TestFiles-20070927.zip",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ocf/internetgateway1",
|
||||
OfficialName: "Internet Gateway Device v1 - Open Connectivity Foundation",
|
||||
Src: openconnectivitydotorg{
|
||||
SpecsURL: ocfSpecsURL,
|
||||
DocPath: "*/DeviceProtection_1/UPnP-gw-*v1*.pdf",
|
||||
XMLSpecZipPath: "*/DeviceProtection_1/UPnP-gw-IGD-TestFiles-*.zip",
|
||||
XMLServicePath: []string{"*/service/*1.xml"},
|
||||
XMLDevicePath: []string{"*/device/*1.xml"},
|
||||
Hacks: []DCPHackFn{
|
||||
fixMissingURN("urn:schemas-upnp-org:service:DeviceProtection:1"),
|
||||
fixMissingURN("urn:schemas-upnp-org:service:WANIPv6FirewallControl:1"),
|
||||
fixTotalBytes(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ocf/internetgateway2",
|
||||
OfficialName: "Internet Gateway Device v2 - Open Connectivity Foundation",
|
||||
Src: openconnectivitydotorg{
|
||||
SpecsURL: ocfSpecsURL,
|
||||
DocPath: "*/Internet Gateway_2/UPnP-gw-*.pdf",
|
||||
XMLSpecZipPath: "*/Internet Gateway_2/UPnP-gw-IGD-TestFiles-*.zip",
|
||||
XMLServicePath: []string{"*/service/*1.xml", "*/service/*2.xml"},
|
||||
XMLDevicePath: []string{"*/device/*1.xml", "*/device/*2.xml"},
|
||||
Hacks: []DCPHackFn{
|
||||
fixMissingURN("urn:schemas-upnp-org:service:DeviceProtection:1"),
|
||||
fixTotalBytes(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func totalBytesHack(dcp *DCP) error {
|
||||
for _, service := range dcp.Services {
|
||||
if service.URN == "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1" {
|
||||
variables := service.SCPD.StateVariables
|
||||
for key, variable := range variables {
|
||||
varName := variable.Name
|
||||
if varName == "TotalBytesSent" || varName == "TotalBytesReceived" {
|
||||
// Fix size of total bytes which is by default ui4 or maximum 4 GiB.
|
||||
variable.DataType.Name = "ui8"
|
||||
variables[key] = variable
|
||||
func fixTotalBytes(malformedURNs ...string) func(dcp *DCP) error {
|
||||
malformedVariables := []string{
|
||||
"TotalBytesSent",
|
||||
"TotalBytesReceived",
|
||||
}
|
||||
return func(dcp *DCP) error {
|
||||
for _, service := range dcp.Services {
|
||||
var process bool
|
||||
for _, malformedURN := range malformedURNs {
|
||||
if service.URN == malformedURN {
|
||||
process = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if process || len(malformedURNs) < 1 {
|
||||
variables := service.SCPD.StateVariables
|
||||
for key, variable := range variables {
|
||||
varName := variable.Name
|
||||
for _, malformedVariable := range malformedVariables {
|
||||
if strings.HasSuffix(varName, malformedVariable) {
|
||||
// Fix size of total bytes which is by default ui4 or maximum 4 GiB.
|
||||
variable.DataType.Name = "ui8"
|
||||
variables[key] = variable
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
func fixMissingURN(missingURNs ...string) func(dcp *DCP) error {
|
||||
return func(dcp *DCP) error {
|
||||
for _, missingURN := range missingURNs {
|
||||
if _, ok := dcp.ServiceTypes[missingURN]; ok {
|
||||
continue
|
||||
}
|
||||
urnParts, err := extractURNParts(missingURN, serviceURNPrefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dcp.ServiceTypes[missingURN] = urnParts
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type DCPHackFn func(*DCP) error
|
||||
|
101
cmd/goupnpdcpgen/provider.go
Normal file
101
cmd/goupnpdcpgen/provider.go
Normal file
@ -0,0 +1,101 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type dcpProvider interface {
|
||||
process(tmpdir, name string, dcp *DCP) error
|
||||
}
|
||||
type upnpdotorg struct {
|
||||
DocURL string // Optional - URL for further documentation about the DCP.
|
||||
XMLSpecURL string // Where to download the XML spec from.
|
||||
// Any special-case functions to run against the DCP before writing it out.
|
||||
Hacks []DCPHackFn
|
||||
}
|
||||
|
||||
func (u upnpdotorg) process(tmpdir, name string, dcp *DCP) error {
|
||||
dcp.DocURLs = append(dcp.DocURLs, u.DocURL)
|
||||
specFilename := filepath.Join(tmpdir, name+".zip")
|
||||
err := acquireFile(specFilename, u.XMLSpecURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not acquire spec for %s: %v", name, err)
|
||||
}
|
||||
archive, err := zip.OpenReader(specFilename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading zip file %q: %v", specFilename, err)
|
||||
}
|
||||
defer archive.Close()
|
||||
if err := dcp.processZipFile(archive.File, []string{"*/device/*.xml"}, []string{"*/service/*.xml"}); err != nil {
|
||||
return fmt.Errorf("error processing spec file %q: %v", specFilename, err)
|
||||
}
|
||||
for i, hack := range u.Hacks {
|
||||
if err := hack(dcp); err != nil {
|
||||
return fmt.Errorf("error with Hack[%d] for %s: %v", i, name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const ocfSpecsURL = "https://openconnectivity.org/upnp-specs/upnpresources.zip"
|
||||
|
||||
type openconnectivitydotorg struct {
|
||||
DocPath string // Optional - Glob to the related documentation about the DCP.
|
||||
SpecsURL string // The HTTP location of the zip archive containing all XML spec.
|
||||
XMLSpecZipPath string // Glob to the zip XML spec file within upnpresources.zip.
|
||||
// Glob to the services XML files within the ZIP matching XMLSpecZipPath.
|
||||
XMLServicePath []string
|
||||
// Glob to the devices XML files within the ZIP matching XMLSpecZipPath.
|
||||
XMLDevicePath []string
|
||||
// Any special-case functions to run against the DCP before writing it out.
|
||||
Hacks []DCPHackFn
|
||||
}
|
||||
|
||||
func (o openconnectivitydotorg) process(tmpdir, name string, dcp *DCP) error {
|
||||
fname := filepath.Base(name)
|
||||
allSpecsFilename := filepath.Join(tmpdir, "openconnectivitydotorg_"+fname+".zip")
|
||||
err := acquireFile(allSpecsFilename, o.SpecsURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not acquire specs %s: %v", name, err)
|
||||
}
|
||||
allSpecsArchive, err := zip.OpenReader(allSpecsFilename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading zip file %q: %v", allSpecsFilename, err)
|
||||
}
|
||||
specsArchives := globFiles(o.XMLSpecZipPath, allSpecsArchive.File)
|
||||
if len(specsArchives) < 1 {
|
||||
return fmt.Errorf("zip archive %q does not contain specifications at %q", allSpecsFilename, o.XMLSpecZipPath)
|
||||
}
|
||||
for _, specArchive := range specsArchives {
|
||||
f, err := specArchive.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading zip file %q: %v", specArchive.Name, err)
|
||||
}
|
||||
defer f.Close()
|
||||
b, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
archive, err := zip.NewReader(bytes.NewReader(b), specArchive.FileInfo().Size())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading zip file %q: %v", specArchive.Name, err)
|
||||
}
|
||||
if err := dcp.processZipFile(archive.File, o.XMLDevicePath, o.XMLServicePath); err != nil {
|
||||
return fmt.Errorf("error processing spec file %q: %v", specArchive.Name, err)
|
||||
}
|
||||
}
|
||||
for i, hack := range o.Hacks {
|
||||
if err := hack(dcp); err != nil {
|
||||
return fmt.Errorf("error with Hack[%d] for %s: %v", i, name, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, d := range globFiles(o.DocPath, allSpecsArchive.File) {
|
||||
dcp.DocURLs = append(dcp.DocURLs, d.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
// Client for UPnP Device Control Protocol MediaServer v1 and MediaRenderer v1.
|
||||
//
|
||||
// This DCP is documented in detail at: http://upnp.org/specs/av/av1/
|
||||
// This DCP is documented in detail at:
|
||||
// - http://upnp.org/specs/av/av1/
|
||||
//
|
||||
// Typically, use one of the New* functions to create clients for services.
|
||||
package av1
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Client for UPnP Device Control Protocol Internet Gateway Device v1.
|
||||
//
|
||||
// This DCP is documented in detail at: http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v1-Device.pdf
|
||||
// This DCP is documented in detail at:
|
||||
// - http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v1-Device.pdf
|
||||
//
|
||||
// Typically, use one of the New* functions to create clients for services.
|
||||
package internetgateway1
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Client for UPnP Device Control Protocol Internet Gateway Device v2.
|
||||
//
|
||||
// This DCP is documented in detail at: http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v2-Device.pdf
|
||||
// This DCP is documented in detail at:
|
||||
// - http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v2-Device.pdf
|
||||
//
|
||||
// Typically, use one of the New* functions to create clients for services.
|
||||
package internetgateway2
|
||||
|
2
dcps/ocf/internetgateway2/gen.go
Normal file
2
dcps/ocf/internetgateway2/gen.go
Normal file
@ -0,0 +1,2 @@
|
||||
//go:generate goupnpdcpgen -dcp_name ocf/internetgateway2
|
||||
package internetgateway2
|
6158
dcps/ocf/internetgateway2/internetgateway2.go
Normal file
6158
dcps/ocf/internetgateway2/internetgateway2.go
Normal file
File diff suppressed because it is too large
Load Diff
2
go.sum
2
go.sum
@ -1,6 +1,4 @@
|
||||
github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150 h1:vlNjIqmUZ9CMAWsbURYl3a6wZbw7q5RHVvlXTNS/Bs8=
|
||||
github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
Loading…
Reference in New Issue
Block a user