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)
+ }
+ }
+}