diff --git a/config/config.go b/config/config.go index 8622994..03f3969 100644 --- a/config/config.go +++ b/config/config.go @@ -2,6 +2,8 @@ package config import ( "fmt" + "math" + "net" "reflect" "github.com/google/go-cmp/cmp" @@ -231,3 +233,42 @@ func storeStructConfigValues(cvs []Value, v reflect.Value) error { type Unmarshaler interface { UnmarshalConfig(Block) error } + +// Port represents a port number in the configuration. When a configuration is +// converted to Go types using Unmarshal, it implements special conversion +// rules: +// If the config option is a numeric value, it is ensured to be in the range +// [1–65535]. If the config option is a string, it is converted to a port +// number using "net".LookupPort (using "tcp" as network). +type Port int + +// UnmarshalConfig converts b to a port number. +func (p *Port) UnmarshalConfig(b Block) error { + if len(b.Values) != 1 || len(b.Children) != 0 { + return fmt.Errorf("option %q has to be a single scalar value", b.Key) + } + + v := b.Values[0] + if f, ok := v.Number(); ok { + if math.IsNaN(f) { + return fmt.Errorf("the value of the %q option (%v) is invalid", b.Key, f) + } + if f < 1 || f > math.MaxUint16 { + return fmt.Errorf("the value of the %q option (%v) is out of range", b.Key, f) + } + *p = Port(f) + return nil + } + + if !v.IsString() { + return fmt.Errorf("the value of the %q option must be a number or a string", b.Key) + } + + port, err := net.LookupPort("tcp", v.String()) + if err != nil { + return fmt.Errorf("%s: %w", b.Key, err) + } + + *p = Port(port) + return nil +} diff --git a/config/config_test.go b/config/config_test.go index 58b40f1..3adf3b6 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "math" "testing" "github.com/google/go-cmp/cmp" @@ -356,6 +357,126 @@ func TestConfig_Unmarshal(t *testing.T) { }{}, wantErr: true, }, + { + name: "port numeric success", + src: Block{ + Key: "Plugin", + Values: []Value{StringValue("test")}, + Children: []Block{ + { + Key: "Port", + Values: []Value{Float64Value(80)}, + }, + }, + }, + dst: &struct { + Args string + Port Port + }{}, + want: &struct { + Args string + Port Port + }{ + Args: "test", + Port: Port(80), + }, + }, + { + name: "port out of range", + src: Block{ + Key: "Plugin", + Values: []Value{StringValue("test")}, + Children: []Block{ + { + Key: "Port", + Values: []Value{Float64Value(1<<48)}, + }, + }, + }, + dst: &struct { + Args string + Port Port + }{}, + wantErr: true, + }, + { + name: "port not a number", + src: Block{ + Key: "Plugin", + Values: []Value{StringValue("test")}, + Children: []Block{ + { + Key: "Port", + Values: []Value{Float64Value(math.NaN())}, + }, + }, + }, + dst: &struct { + Args string + Port Port + }{}, + wantErr: true, + }, + { + name: "port invalid type", + src: Block{ + Key: "Plugin", + Values: []Value{StringValue("test")}, + Children: []Block{ + { + Key: "Port", + Values: []Value{BoolValue(true)}, + }, + }, + }, + dst: &struct { + Args string + Port Port + }{}, + wantErr: true, + }, + { + name: "port string success", + src: Block{ + Key: "Plugin", + Values: []Value{StringValue("test")}, + Children: []Block{ + { + Key: "Port", + Values: []Value{StringValue("http")}, + }, + }, + }, + dst: &struct { + Args string + Port Port + }{}, + want: &struct { + Args string + Port Port + }{ + Args: "test", + Port: Port(80), + }, + }, + { + name: "port string failure", + src: Block{ + Key: "Plugin", + Values: []Value{StringValue("test")}, + Children: []Block{ + { + Key: "Port", + Values: []Value{StringValue("--- invalid ---")}, + }, + }, + }, + dst: &struct { + Args string + Port Port + }{}, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {