diff --git a/env.go b/env.go index c3ac260a..8cbc336c 100644 --- a/env.go +++ b/env.go @@ -379,7 +379,7 @@ func doParseField( if !refField.CanSet() { return nil } - if refField.Kind() == reflect.Pointer && refField.Elem().Kind() == reflect.Struct && !refField.IsNil() { + if refField.Kind() == reflect.Pointer && refField.Elem().Kind() == reflect.Struct && !refField.IsNil() && !hasCustomOrTextParserType(refTypeField.Type, opts.FuncMap) { return parseInternal(refField.Interface(), processField, optionsWithEnvPrefix(refTypeField, opts)) } if refField.Kind() == reflect.Struct && refField.CanAddr() && refField.Type().Name() == "" { @@ -404,31 +404,31 @@ func doParseField( refField = refField.Elem() } - if refField.Kind() == reflect.Struct { + if refField.Kind() == reflect.Struct && !hasCustomOrTextParserType(refTypeField.Type, opts.FuncMap) { return doParse(refField, processField, optionsWithEnvPrefix(refTypeField, opts)) } - if isSliceOfStructs(refTypeField) { + if isSliceOfStructs(refTypeField, opts) { return doParseSlice(refField, processField, optionsWithEnvPrefix(refTypeField, opts)) } return nil } -func isSliceOfStructs(refTypeField reflect.StructField) bool { +func isSliceOfStructs(refTypeField reflect.StructField, opts Options) bool { field := refTypeField.Type // *[]struct if field.Kind() == reflect.Pointer { field = field.Elem() - if field.Kind() == reflect.Slice && field.Elem().Kind() == reflect.Struct { - return true - } } - // []struct{} if field.Kind() == reflect.Slice && field.Elem().Kind() == reflect.Struct { - return true + elemType := field.Elem() + if elemType.Kind() == reflect.Pointer { + elemType = elemType.Elem() + } + return !hasCustomOrTextParserType(elemType, opts.FuncMap) } return false @@ -851,3 +851,18 @@ func ToMap(env []string) map[string]string { func isInvalidPtr(v reflect.Value) bool { return reflect.Pointer == v.Kind() && v.Elem().Kind() == reflect.Invalid } + +var textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() //nolint:gochecknoglobals + +func hasCustomOrTextParserType(t reflect.Type, funcMap map[reflect.Type]ParserFunc) bool { + if t.Kind() == reflect.Pointer { + t = t.Elem() + } + if _, ok := funcMap[t]; ok { + return true + } + if t.Implements(textUnmarshalerType) || reflect.PointerTo(t).Implements(textUnmarshalerType) { + return true + } + return false +} diff --git a/env_test.go b/env_test.go index 2fe79c31..b66b74fd 100644 --- a/env_test.go +++ b/env_test.go @@ -2417,3 +2417,64 @@ func TestEnvBleed(t *testing.T) { isEqual(t, "", cfg.Foo) }) } + +type customTextUnmarshaler struct { + Value string +} + +func (c *customTextUnmarshaler) UnmarshalText(text []byte) error { + c.Value = string(text) + return nil +} + +func TestUseFieldNameByDefault_URLAndTextUnmarshaler(t *testing.T) { + type config struct { + URL url.URL `env:"URL"` + Custom customTextUnmarshaler `env:"CUSTOM"` + CustomPtr *customTextUnmarshaler `env:"CUSTOM_PTR"` + } + + environ := map[string]string{ + "URL": "http://example.com/a/b", + "PATH": "/usr/bin", + "HOST": "evil", + "CUSTOM": "custom-value", + "CUSTOM_PTR": "custom-ptr-value", + "VALUE": "evil-nested", + } + + t.Run("by tag", func(t *testing.T) { + var cfg config + err := ParseWithOptions(&cfg, Options{Environment: environ}) + isNoErr(t, err) + isEqual(t, "http://example.com/a/b", cfg.URL.String()) + isEqual(t, "custom-value", cfg.Custom.Value) + isEqual(t, "custom-ptr-value", cfg.CustomPtr.Value) + }) + + t.Run("with UseFieldNameByDefault", func(t *testing.T) { + var cfg config + err := ParseWithOptions(&cfg, Options{Environment: environ, UseFieldNameByDefault: true}) + isNoErr(t, err) + isEqual(t, "http://example.com/a/b", cfg.URL.String()) + isEqual(t, "custom-value", cfg.Custom.Value) + isEqual(t, "custom-ptr-value", cfg.CustomPtr.Value) + }) + + t.Run("with UseFieldNameByDefault and RequiredIfNoDef", func(t *testing.T) { + var cfg config + err := ParseWithOptions(&cfg, Options{ + Environment: map[string]string{ + "URL": "http://example.com/a/b", + "CUSTOM": "custom-value", + "CUSTOM_PTR": "custom-ptr-value", + }, + UseFieldNameByDefault: true, + RequiredIfNoDef: true, + }) + isNoErr(t, err) + isEqual(t, "http://example.com/a/b", cfg.URL.String()) + isEqual(t, "custom-value", cfg.Custom.Value) + isEqual(t, "custom-ptr-value", cfg.CustomPtr.Value) + }) +}