diff --git a/lib/config/config.go b/lib/config/config.go index 1d73db38c..4f80fd180 100644 --- a/lib/config/config.go +++ b/lib/config/config.go @@ -11,15 +11,12 @@ import ( "encoding/json" "encoding/xml" "io" - "math/rand" - "net/url" "os" - "reflect" "sort" - "strconv" "strings" "github.com/syncthing/syncthing/lib/protocol" + "github.com/syncthing/syncthing/lib/util" ) const ( @@ -58,9 +55,9 @@ func New(myID protocol.DeviceID) Configuration { cfg.Version = CurrentVersion cfg.OriginalVersion = CurrentVersion - setDefaults(&cfg) - setDefaults(&cfg.Options) - setDefaults(&cfg.GUI) + util.SetDefaults(&cfg) + util.SetDefaults(&cfg.Options) + util.SetDefaults(&cfg.GUI) cfg.prepare(myID) @@ -70,9 +67,9 @@ func New(myID protocol.DeviceID) Configuration { func ReadXML(r io.Reader, myID protocol.DeviceID) (Configuration, error) { var cfg Configuration - setDefaults(&cfg) - setDefaults(&cfg.Options) - setDefaults(&cfg.GUI) + util.SetDefaults(&cfg) + util.SetDefaults(&cfg.Options) + util.SetDefaults(&cfg.GUI) err := xml.NewDecoder(r).Decode(&cfg) cfg.OriginalVersion = cfg.Version @@ -84,9 +81,9 @@ func ReadXML(r io.Reader, myID protocol.DeviceID) (Configuration, error) { func ReadJSON(r io.Reader, myID protocol.DeviceID) (Configuration, error) { var cfg Configuration - setDefaults(&cfg) - setDefaults(&cfg.Options) - setDefaults(&cfg.GUI) + util.SetDefaults(&cfg) + util.SetDefaults(&cfg.Options) + util.SetDefaults(&cfg.GUI) err := json.NewDecoder(r).Decode(&cfg) cfg.OriginalVersion = cfg.Version @@ -143,7 +140,7 @@ func (cfg *Configuration) WriteXML(w io.Writer) error { } func (cfg *Configuration) prepare(myID protocol.DeviceID) { - fillNilSlices(&cfg.Options) + util.FillNilSlices(&cfg.Options) // Initialize any empty slices if cfg.Folders == nil { @@ -171,8 +168,8 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) { } } - cfg.Options.ListenAddress = uniqueStrings(cfg.Options.ListenAddress) - cfg.Options.GlobalAnnServers = uniqueStrings(cfg.Options.GlobalAnnServers) + cfg.Options.ListenAddress = util.UniqueStrings(cfg.Options.ListenAddress) + cfg.Options.GlobalAnnServers = util.UniqueStrings(cfg.Options.GlobalAnnServers) if cfg.Version > 0 && cfg.Version < OldestHandledVersion { l.Warnf("Configuration version %d is deprecated. Attempting best effort conversion, but please verify manually.", cfg.Version) @@ -234,7 +231,7 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) { } if cfg.GUI.APIKey == "" { - cfg.GUI.APIKey = randomString(32) + cfg.GUI.APIKey = util.RandomString(32) } } @@ -242,14 +239,14 @@ func convertV11V12(cfg *Configuration) { // Change listen address schema for i, addr := range cfg.Options.ListenAddress { if len(addr) > 0 && !strings.HasPrefix(addr, "tcp://") { - cfg.Options.ListenAddress[i] = tcpAddr(addr) + cfg.Options.ListenAddress[i] = util.Address("tcp", addr) } } for i, device := range cfg.Devices { for j, addr := range device.Addresses { if addr != "dynamic" && addr != "" { - cfg.Devices[i].Addresses[j] = tcpAddr(addr) + cfg.Devices[i].Addresses[j] = util.Address("tcp", addr) } } } @@ -297,98 +294,6 @@ func convertV10V11(cfg *Configuration) { cfg.Version = 11 } -func setDefaults(data interface{}) error { - s := reflect.ValueOf(data).Elem() - t := s.Type() - - for i := 0; i < s.NumField(); i++ { - f := s.Field(i) - tag := t.Field(i).Tag - - v := tag.Get("default") - if len(v) > 0 { - switch f.Interface().(type) { - case string: - f.SetString(v) - - case int: - i, err := strconv.ParseInt(v, 10, 64) - if err != nil { - return err - } - f.SetInt(i) - - case float64: - i, err := strconv.ParseFloat(v, 64) - if err != nil { - return err - } - f.SetFloat(i) - - case bool: - f.SetBool(v == "true") - - case []string: - // We don't do anything with string slices here. Any default - // we set will be appended to by the XML decoder, so we fill - // those after decoding. - - default: - panic(f.Type()) - } - } - } - return nil -} - -// fillNilSlices sets default value on slices that are still nil. -func fillNilSlices(data interface{}) error { - s := reflect.ValueOf(data).Elem() - t := s.Type() - - for i := 0; i < s.NumField(); i++ { - f := s.Field(i) - tag := t.Field(i).Tag - - v := tag.Get("default") - if len(v) > 0 { - switch f.Interface().(type) { - case []string: - if f.IsNil() { - // Treat the default as a comma separated slice - vs := strings.Split(v, ",") - for i := range vs { - vs[i] = strings.TrimSpace(vs[i]) - } - - rv := reflect.MakeSlice(reflect.TypeOf([]string{}), len(vs), len(vs)) - for i, v := range vs { - rv.Index(i).SetString(v) - } - f.Set(rv) - } - } - } - } - return nil -} - -func uniqueStrings(ss []string) []string { - var m = make(map[string]bool, len(ss)) - for _, s := range ss { - m[strings.Trim(s, " ")] = true - } - - var us = make([]string, 0, len(m)) - for k := range m { - us = append(us, k) - } - - sort.Strings(us) - - return us -} - func ensureDevicePresent(devices []FolderDeviceConfiguration, myID protocol.DeviceID) []FolderDeviceConfiguration { for _, device := range devices { if device.DeviceID.Equals(myID) { @@ -453,24 +358,3 @@ loop: } return devices[0:count] } - -// randomCharset contains the characters that can make up a randomString(). -const randomCharset = "01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-" - -// randomString returns a string of random characters (taken from -// randomCharset) of the specified length. -func randomString(l int) string { - bs := make([]byte, l) - for i := range bs { - bs[i] = randomCharset[rand.Intn(len(randomCharset))] - } - return string(bs) -} - -func tcpAddr(host string) string { - u := url.URL{ - Scheme: "tcp", - Host: host, - } - return u.String() -} diff --git a/lib/config/wrapper.go b/lib/config/wrapper.go index 30c73ecdd..fc12ca324 100644 --- a/lib/config/wrapper.go +++ b/lib/config/wrapper.go @@ -13,6 +13,7 @@ import ( "github.com/syncthing/syncthing/lib/osutil" "github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/sync" + "github.com/syncthing/syncthing/lib/util" ) // The Committer interface is implemented by objects that need to know about @@ -321,5 +322,5 @@ func (w *Wrapper) GlobalDiscoveryServers() []string { servers = append(servers, srv) } } - return uniqueStrings(servers) + return util.UniqueStrings(servers) } diff --git a/lib/connections/connections.go b/lib/connections/connections.go index 9b6f4fe5d..007aba1e8 100644 --- a/lib/connections/connections.go +++ b/lib/connections/connections.go @@ -24,6 +24,7 @@ import ( "github.com/syncthing/syncthing/lib/relay" "github.com/syncthing/syncthing/lib/relay/client" "github.com/syncthing/syncthing/lib/upnp" + "github.com/syncthing/syncthing/lib/util" "github.com/thejerf/suture" ) @@ -504,7 +505,7 @@ func (s *Service) addresses(includePrivateIPV4 bool) []string { l.Infoln("Listen address", addrStr, "is invalid:", err) continue } - addr, err := net.ResolveTCPAddr("tcp", addrURL.Host) + addr, err := net.ResolveTCPAddr(addrURL.Scheme, addrURL.Host) if err != nil { l.Infoln("Listen address", addrStr, "is invalid:", err) continue @@ -512,13 +513,13 @@ func (s *Service) addresses(includePrivateIPV4 bool) []string { if addr.IP == nil || addr.IP.IsUnspecified() { // Address like 0.0.0.0:22000 or [::]:22000 or :22000; include as is. - addrs = append(addrs, tcpAddr(addr.String())) + addrs = append(addrs, util.Address(addrURL.Scheme, addr.String())) } else if isPublicIPv4(addr.IP) || isPublicIPv6(addr.IP) { // A public address; include as is. - addrs = append(addrs, tcpAddr(addr.String())) + addrs = append(addrs, util.Address(addrURL.Scheme, addr.String())) } else if includePrivateIPV4 && addr.IP.To4().IsGlobalUnicast() { // A private IPv4 address. - addrs = append(addrs, tcpAddr(addr.String())) + addrs = append(addrs, util.Address(addrURL.Scheme, addr.String())) } } @@ -567,14 +568,6 @@ func isPublicIPv6(ip net.IP) bool { return ip.IsGlobalUnicast() } -func tcpAddr(host string) string { - u := url.URL{ - Scheme: "tcp", - Host: host, - } - return u.String() -} - // serviceFunc wraps a function to create a suture.Service without stop // functionality. type serviceFunc func() diff --git a/lib/upnp/debug.go b/lib/upnp/debug.go index f173e5be9..adc60fe58 100644 --- a/lib/upnp/debug.go +++ b/lib/upnp/debug.go @@ -20,7 +20,3 @@ var ( func init() { l.SetDebug("upnp", strings.Contains(os.Getenv("STTRACE"), "upnp") || os.Getenv("STTRACE") == "all") } - -func shouldDebug() bool { - return l.ShouldDebug("upnp") -} diff --git a/lib/upnp/igd.go b/lib/upnp/igd.go new file mode 100644 index 000000000..695015213 --- /dev/null +++ b/lib/upnp/igd.go @@ -0,0 +1,91 @@ +// Copyright (C) 2016 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at http://mozilla.org/MPL/2.0/. + +// Adapted from https://github.com/jackpal/Taipei-Torrent/blob/dd88a8bfac6431c01d959ce3c745e74b8a911793/IGD.go +// Copyright (c) 2010 Jack Palevich (https://github.com/jackpal/Taipei-Torrent/blob/dd88a8bfac6431c01d959ce3c745e74b8a911793/LICENSE) + +package upnp + +import ( + "net" + "net/url" + "strings" +) + +// An IGD is a UPnP InternetGatewayDevice. +type IGD struct { + uuid string + friendlyName string + services []IGDService + url *url.URL + localIPAddress net.IP +} + +func (n *IGD) UUID() string { + return n.uuid +} + +func (n *IGD) FriendlyName() string { + return n.friendlyName +} + +// FriendlyIdentifier returns a friendly identifier (friendly name + IP +// address) for the IGD. +func (n *IGD) FriendlyIdentifier() string { + return "'" + n.FriendlyName() + "' (" + strings.Split(n.URL().Host, ":")[0] + ")" +} + +func (n *IGD) URL() *url.URL { + return n.url +} + +// AddPortMapping adds a port mapping to all relevant services on the +// specified InternetGatewayDevice. Port mapping will fail and return an error +// if action is fails for _any_ of the relevant services. For this reason, it +// is generally better to configure port mapping for each individual service +// instead. +func (n *IGD) AddPortMapping(protocol Protocol, externalPort, internalPort int, description string, timeout int) error { + for _, service := range n.services { + err := service.AddPortMapping(n.localIPAddress, protocol, externalPort, internalPort, description, timeout) + if err != nil { + return err + } + } + return nil +} + +// DeletePortMapping deletes a port mapping from all relevant services on the +// specified InternetGatewayDevice. Port mapping will fail and return an error +// if action is fails for _any_ of the relevant services. For this reason, it +// is generally better to configure port mapping for each individual service +// instead. +func (n *IGD) DeletePortMapping(protocol Protocol, externalPort int) error { + for _, service := range n.services { + err := service.DeletePortMapping(protocol, externalPort) + if err != nil { + return err + } + } + return nil +} + +// GetExternalIPAddress returns the external IP address of the IGD, or an error +// if no service providing this feature exists. +func (n *IGD) GetExternalIPAddress() (ip net.IP, err error) { + for _, service := range n.services { + ip, err = service.GetExternalIPAddress() + if err == nil { + break + } + } + return +} + +// GetLocalIPAddress returns the IP address of the local network interface +// which is facing the IGD. +func (n *IGD) GetLocalIPAddress() net.IP { + return n.localIPAddress +} diff --git a/lib/upnp/igd_service.go b/lib/upnp/igd_service.go new file mode 100644 index 000000000..c9a1133be --- /dev/null +++ b/lib/upnp/igd_service.go @@ -0,0 +1,95 @@ +// Copyright (C) 2016 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at http://mozilla.org/MPL/2.0/. + +// Adapted from https://github.com/jackpal/Taipei-Torrent/blob/dd88a8bfac6431c01d959ce3c745e74b8a911793/IGD.go +// Copyright (c) 2010 Jack Palevich (https://github.com/jackpal/Taipei-Torrent/blob/dd88a8bfac6431c01d959ce3c745e74b8a911793/LICENSE) + +package upnp + +import ( + "encoding/xml" + "fmt" + "net" +) + +// An IGDService is a specific service provided by an IGD. +type IGDService struct { + ID string + URL string + URN string +} + +// AddPortMapping adds a port mapping to the specified IGD service. +func (s *IGDService) AddPortMapping(localIPAddress net.IP, protocol Protocol, externalPort, internalPort int, description string, timeout int) error { + tpl := ` + + %d + %s + %d + %s + 1 + %s + %d + ` + body := fmt.Sprintf(tpl, s.URN, externalPort, protocol, internalPort, localIPAddress, description, timeout) + + response, err := soapRequest(s.URL, s.URN, "AddPortMapping", body) + if err != nil && timeout > 0 { + // Try to repair error code 725 - OnlyPermanentLeasesSupported + envelope := &soapErrorResponse{} + if unmarshalErr := xml.Unmarshal(response, envelope); unmarshalErr != nil { + return unmarshalErr + } + if envelope.ErrorCode == 725 { + return s.AddPortMapping(localIPAddress, protocol, externalPort, internalPort, description, 0) + } + } + + return err +} + +// DeletePortMapping deletes a port mapping from the specified IGD service. +func (s *IGDService) DeletePortMapping(protocol Protocol, externalPort int) error { + tpl := ` + + %d + %s + ` + body := fmt.Sprintf(tpl, s.URN, externalPort, protocol) + + _, err := soapRequest(s.URL, s.URN, "DeletePortMapping", body) + + if err != nil { + return err + } + + return nil +} + +// GetExternalIPAddress queries the IGD service for its external IP address. +// Returns nil if the external IP address is invalid or undefined, along with +// any relevant errors +func (s *IGDService) GetExternalIPAddress() (net.IP, error) { + tpl := `` + + body := fmt.Sprintf(tpl, s.URN) + + response, err := soapRequest(s.URL, s.URN, "GetExternalIPAddress", body) + + if err != nil { + return nil, err + } + + envelope := &soapGetExternalIPAddressResponseEnvelope{} + err = xml.Unmarshal(response, envelope) + if err != nil { + return nil, err + } + + result := net.ParseIP(envelope.Body.GetExternalIPAddressResponse.NewExternalIPAddress) + + return result, nil +} diff --git a/lib/upnp/upnp.go b/lib/upnp/upnp.go index 820650e15..7d1528297 100644 --- a/lib/upnp/upnp.go +++ b/lib/upnp/upnp.go @@ -29,40 +29,6 @@ import ( "github.com/syncthing/syncthing/lib/sync" ) -// An IGD is a UPnP InternetGatewayDevice. -type IGD struct { - uuid string - friendlyName string - services []IGDService - url *url.URL - localIPAddress string -} - -func (n *IGD) UUID() string { - return n.uuid -} - -func (n *IGD) FriendlyName() string { - return n.friendlyName -} - -// FriendlyIdentifier returns a friendly identifier (friendly name + IP -// address) for the IGD. -func (n *IGD) FriendlyIdentifier() string { - return "'" + n.FriendlyName() + "' (" + strings.Split(n.URL().Host, ":")[0] + ")" -} - -func (n *IGD) URL() *url.URL { - return n.url -} - -// An IGDService is a specific service provided by an IGD. -type IGDService struct { - ID string - URL string - URN string -} - type Protocol string const ( @@ -126,22 +92,18 @@ nextResult: for result := range resultChan { for _, existingResult := range results { if existingResult.uuid == result.uuid { - if shouldDebug() { - l.Debugf("Skipping duplicate result %s with services:", result.uuid) - for _, service := range result.services { - l.Debugf("* [%s] %s", service.ID, service.URL) - } + l.Debugf("Skipping duplicate result %s with services:", result.uuid) + for _, service := range result.services { + l.Debugf("* [%s] %s", service.ID, service.URL) } continue nextResult } } results = append(results, result) - if shouldDebug() { - l.Debugf("UPnP discovery result %s with services:", result.uuid) - for _, service := range result.services { - l.Debugf("* [%s] %s", service.ID, service.URL) - } + l.Debugf("UPnP discovery result %s with services:", result.uuid) + for _, service := range result.services { + l.Debugf("* [%s] %s", service.ID, service.URL) } } @@ -286,19 +248,19 @@ func parseResponse(deviceType string, resp []byte) (IGD, error) { }, nil } -func localIP(url *url.URL) (string, error) { - conn, err := dialer.Dial("tcp", url.Host) +func localIP(url *url.URL) (net.IP, error) { + conn, err := dialer.DialTimeout("tcp", url.Host, time.Second) if err != nil { - return "", err + return nil, err } defer conn.Close() localIPAddress, _, err := net.SplitHostPort(conn.LocalAddr().String()) if err != nil { - return "", err + return nil, err } - return localIPAddress, nil + return net.ParseIP(localIPAddress), nil } func getChildDevices(d upnpDevice, deviceType string) []upnpDevice { @@ -460,36 +422,6 @@ func soapRequest(url, service, function, message string) ([]byte, error) { return resp, nil } -// AddPortMapping adds a port mapping to all relevant services on the -// specified InternetGatewayDevice. Port mapping will fail and return an error -// if action is fails for _any_ of the relevant services. For this reason, it -// is generally better to configure port mapping for each individual service -// instead. -func (n *IGD) AddPortMapping(protocol Protocol, externalPort, internalPort int, description string, timeout int) error { - for _, service := range n.services { - err := service.AddPortMapping(n.localIPAddress, protocol, externalPort, internalPort, description, timeout) - if err != nil { - return err - } - } - return nil -} - -// DeletePortMapping deletes a port mapping from all relevant services on the -// specified InternetGatewayDevice. Port mapping will fail and return an error -// if action is fails for _any_ of the relevant services. For this reason, it -// is generally better to configure port mapping for each individual service -// instead. -func (n *IGD) DeletePortMapping(protocol Protocol, externalPort int) error { - for _, service := range n.services { - err := service.DeletePortMapping(protocol, externalPort) - if err != nil { - return err - } - } - return nil -} - type soapGetExternalIPAddressResponseEnvelope struct { XMLName xml.Name Body soapGetExternalIPAddressResponseBody `xml:"Body"` @@ -508,75 +440,3 @@ type soapErrorResponse struct { ErrorCode int `xml:"Body>Fault>detail>UPnPError>errorCode"` ErrorDescription string `xml:"Body>Fault>detail>UPnPError>errorDescription"` } - -// AddPortMapping adds a port mapping to the specified IGD service. -func (s *IGDService) AddPortMapping(localIPAddress string, protocol Protocol, externalPort, internalPort int, description string, timeout int) error { - tpl := ` - - %d - %s - %d - %s - 1 - %s - %d - ` - body := fmt.Sprintf(tpl, s.URN, externalPort, protocol, internalPort, localIPAddress, description, timeout) - - response, err := soapRequest(s.URL, s.URN, "AddPortMapping", body) - if err != nil && timeout > 0 { - // Try to repair error code 725 - OnlyPermanentLeasesSupported - envelope := &soapErrorResponse{} - if unmarshalErr := xml.Unmarshal(response, envelope); unmarshalErr != nil { - return unmarshalErr - } - if envelope.ErrorCode == 725 { - return s.AddPortMapping(localIPAddress, protocol, externalPort, internalPort, description, 0) - } - } - - return err -} - -// DeletePortMapping deletes a port mapping from the specified IGD service. -func (s *IGDService) DeletePortMapping(protocol Protocol, externalPort int) error { - tpl := ` - - %d - %s - ` - body := fmt.Sprintf(tpl, s.URN, externalPort, protocol) - - _, err := soapRequest(s.URL, s.URN, "DeletePortMapping", body) - - if err != nil { - return err - } - - return nil -} - -// GetExternalIPAddress queries the IGD service for its external IP address. -// Returns nil if the external IP address is invalid or undefined, along with -// any relevant errors -func (s *IGDService) GetExternalIPAddress() (net.IP, error) { - tpl := `` - - body := fmt.Sprintf(tpl, s.URN) - - response, err := soapRequest(s.URL, s.URN, "GetExternalIPAddress", body) - - if err != nil { - return nil, err - } - - envelope := &soapGetExternalIPAddressResponseEnvelope{} - err = xml.Unmarshal(response, envelope) - if err != nil { - return nil, err - } - - result := net.ParseIP(envelope.Body.GetExternalIPAddressResponse.NewExternalIPAddress) - - return result, nil -} diff --git a/lib/util/utils.go b/lib/util/utils.go new file mode 100644 index 000000000..353cd8038 --- /dev/null +++ b/lib/util/utils.go @@ -0,0 +1,119 @@ +// Copyright (C) 2016 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at http://mozilla.org/MPL/2.0/. + +package util + +import ( + "net/url" + "reflect" + "sort" + "strconv" + "strings" +) + +// SetDefaults sets default values on a struct, based on the default annotation. +func SetDefaults(data interface{}) error { + s := reflect.ValueOf(data).Elem() + t := s.Type() + + for i := 0; i < s.NumField(); i++ { + f := s.Field(i) + tag := t.Field(i).Tag + + v := tag.Get("default") + if len(v) > 0 { + switch f.Interface().(type) { + case string: + f.SetString(v) + + case int: + i, err := strconv.ParseInt(v, 10, 64) + if err != nil { + return err + } + f.SetInt(i) + + case float64: + i, err := strconv.ParseFloat(v, 64) + if err != nil { + return err + } + f.SetFloat(i) + + case bool: + f.SetBool(v == "true") + + case []string: + // We don't do anything with string slices here. Any default + // we set will be appended to by the XML decoder, so we fill + // those after decoding. + + default: + panic(f.Type()) + } + } + } + return nil +} + +// UniqueStrings returns a list on unique strings, trimming and sorting them +// at the same time. +func UniqueStrings(ss []string) []string { + var m = make(map[string]bool, len(ss)) + for _, s := range ss { + m[strings.Trim(s, " ")] = true + } + + var us = make([]string, 0, len(m)) + for k := range m { + us = append(us, k) + } + + sort.Strings(us) + + return us +} + +// FillNilSlices sets default value on slices that are still nil. +func FillNilSlices(data interface{}) error { + s := reflect.ValueOf(data).Elem() + t := s.Type() + + for i := 0; i < s.NumField(); i++ { + f := s.Field(i) + tag := t.Field(i).Tag + + v := tag.Get("default") + if len(v) > 0 { + switch f.Interface().(type) { + case []string: + if f.IsNil() { + // Treat the default as a comma separated slice + vs := strings.Split(v, ",") + for i := range vs { + vs[i] = strings.TrimSpace(vs[i]) + } + + rv := reflect.MakeSlice(reflect.TypeOf([]string{}), len(vs), len(vs)) + for i, v := range vs { + rv.Index(i).SetString(v) + } + f.Set(rv) + } + } + } + } + return nil +} + +// Address constructs a URL from the given network and hostname. +func Address(network, host string) string { + u := url.URL{ + Scheme: network, + Host: host, + } + return u.String() +} diff --git a/lib/util/utils_test.go b/lib/util/utils_test.go new file mode 100644 index 000000000..342d16c35 --- /dev/null +++ b/lib/util/utils_test.go @@ -0,0 +1,158 @@ +// Copyright (C) 2016 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at http://mozilla.org/MPL/2.0/. + +package util + +import "testing" + +func TestSetDefaults(t *testing.T) { + x := &struct { + A string `default:"string"` + B int `default:"2"` + C float64 `default:"2.2"` + D bool `default:"true"` + }{} + + if x.A != "" { + t.Error("string failed") + } else if x.B != 0 { + t.Error("int failed") + } else if x.C != 0 { + t.Errorf("float failed") + } else if x.D != false { + t.Errorf("bool failed") + } + + if err := SetDefaults(x); err != nil { + t.Error(err) + } + + if x.A != "string" { + t.Error("string failed") + } else if x.B != 2 { + t.Error("int failed") + } else if x.C != 2.2 { + t.Errorf("float failed") + } else if x.D != true { + t.Errorf("bool failed") + } +} + +func TestUniqueStrings(t *testing.T) { + tests := []struct { + input []string + expected []string + }{ + { + []string{"a", "b"}, + []string{"a", "b"}, + }, + { + []string{"a", "a"}, + []string{"a"}, + }, + { + []string{"a", "a", "a", "a"}, + []string{"a"}, + }, + { + nil, + nil, + }, + { + []string{"b", "a"}, + []string{"a", "b"}, + }, + { + []string{" a ", " a ", "b ", " b"}, + []string{"a", "b"}, + }, + } + + for _, test := range tests { + result := UniqueStrings(test.input) + if len(result) != len(test.expected) { + t.Errorf("%s != %s", result, test.expected) + } + for i := range result { + if test.expected[i] != result[i] { + t.Errorf("%s != %s", result, test.expected) + } + } + } +} + +func TestFillNillSlices(t *testing.T) { + // Nil + x := &struct { + A []string `default:"a,b"` + }{} + + if x.A != nil { + t.Error("not nil") + } + + if err := FillNilSlices(x); err != nil { + t.Error(err) + } + + if len(x.A) != 2 { + t.Error("length") + } + + // Already provided + y := &struct { + A []string `default:"c,d,e"` + }{[]string{"a", "b"}} + + if len(y.A) != 2 { + t.Error("length") + } + + if err := FillNilSlices(y); err != nil { + t.Error(err) + } + + if len(y.A) != 2 { + t.Error("length") + } + + // Non-nil but empty + z := &struct { + A []string `default:"c,d,e"` + }{[]string{}} + + if len(z.A) != 0 { + t.Error("length") + } + + if err := FillNilSlices(z); err != nil { + t.Error(err) + } + + if len(z.A) != 0 { + t.Error("length") + } +} + +func TestAddress(t *testing.T) { + tests := []struct { + network string + host string + result string + }{ + {"tcp", "google.com", "tcp://google.com"}, + {"foo", "google", "foo://google"}, + {"123", "456", "123://456"}, + } + + for _, test := range tests { + result := Address(test.network, test.host) + if result != test.result { + t.Errorf("%s != %s", result, test.result) + } + } +}