From eb8f2b93f1455b6a07755db5f70f3276d17b5130 Mon Sep 17 00:00:00 2001 From: DeadLockStarve Date: Thu, 13 Nov 2025 18:17:14 +0100 Subject: [PATCH 1/2] Implementing vscode suggestions and updating to latest go api calls --- data_source.go | 5 ++--- file.go | 3 +-- file_test.go | 6 +++--- ini_test.go | 4 ++-- key.go | 2 +- parser.go | 5 +++-- 6 files changed, 12 insertions(+), 13 deletions(-) diff --git a/data_source.go b/data_source.go index c3a541f..6e2572b 100644 --- a/data_source.go +++ b/data_source.go @@ -18,7 +18,6 @@ import ( "bytes" "fmt" "io" - "io/ioutil" "os" ) @@ -48,7 +47,7 @@ type sourceData struct { } func (s *sourceData) ReadCloser() (io.ReadCloser, error) { - return ioutil.NopCloser(bytes.NewReader(s.data)), nil + return io.NopCloser(bytes.NewReader(s.data)), nil } // sourceReadCloser represents an input stream with Close method. @@ -69,7 +68,7 @@ func parseDataSource(source interface{}) (dataSource, error) { case io.ReadCloser: return &sourceReadCloser{s}, nil case io.Reader: - return &sourceReadCloser{ioutil.NopCloser(s)}, nil + return &sourceReadCloser{io.NopCloser(s)}, nil default: return nil, fmt.Errorf("error parsing data source: unknown type %q", s) } diff --git a/file.go b/file.go index f8b2240..58beabc 100644 --- a/file.go +++ b/file.go @@ -19,7 +19,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "os" "strings" "sync" @@ -532,7 +531,7 @@ func (f *File) SaveToIndent(filename, indent string) error { return err } - return ioutil.WriteFile(filename, buf.Bytes(), 0666) + return os.WriteFile(filename, buf.Bytes(), 0666) } // SaveTo writes content to file system. diff --git a/file_test.go b/file_test.go index c9914b5..306c0c8 100644 --- a/file_test.go +++ b/file_test.go @@ -16,7 +16,7 @@ package ini import ( "bytes" - "io/ioutil" + "os" "runtime" "sort" "testing" @@ -421,10 +421,10 @@ func TestFile_WriteTo(t *testing.T) { golden := "testdata/TestFile_WriteTo.golden" if *update { - require.NoError(t, ioutil.WriteFile(golden, buf.Bytes(), 0644)) + require.NoError(t, os.WriteFile(golden, buf.Bytes(), 0644)) } - expected, err := ioutil.ReadFile(golden) + expected, err := os.ReadFile(golden) require.NoError(t, err) assert.Equal(t, string(expected), buf.String()) }) diff --git a/ini_test.go b/ini_test.go index 306a2a8..282dc03 100644 --- a/ini_test.go +++ b/ini_test.go @@ -17,7 +17,7 @@ package ini import ( "bytes" "flag" - "io/ioutil" + "io" "path/filepath" "runtime" "testing" @@ -58,7 +58,7 @@ func TestLoad(t *testing.T) { "testdata/minimal.ini", []byte("NAME = ini\nIMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s"), bytes.NewReader([]byte(`VERSION = v1`)), - ioutil.NopCloser(bytes.NewReader([]byte("[author]\nNAME = Unknwon"))), + io.NopCloser(bytes.NewReader([]byte("[author]\nNAME = Unknwon"))), ) require.NoError(t, err) require.NotNil(t, f) diff --git a/key.go b/key.go index a19d9f3..9cbfa1f 100644 --- a/key.go +++ b/key.go @@ -429,7 +429,7 @@ func (k *Key) InUint64(defaultVal uint64, candidates []uint64) uint64 { func (k *Key) InTimeFormat(format string, defaultVal time.Time, candidates []time.Time) time.Time { val := k.MustTimeFormat(format) for _, cand := range candidates { - if val == cand { + if val.Equal(cand) { return val } } diff --git a/parser.go b/parser.go index 44fc526..4cbe363 100644 --- a/parser.go +++ b/parser.go @@ -130,13 +130,14 @@ func readKeyName(delimiters string, in []byte) (string, int, error) { // Check if key name surrounded by quotes. var keyQuote string - if line[0] == '"' { + switch line[0] { + case '"': if len(line) > 6 && line[0:3] == `"""` { keyQuote = `"""` } else { keyQuote = `"` } - } else if line[0] == '`' { + case '`': keyQuote = "`" } From 65fefbf967fdf2d742221adf1270003f09326619 Mon Sep 17 00:00:00 2001 From: DeadLockStarve Date: Fri, 14 Nov 2025 16:12:38 +0100 Subject: [PATCH 2/2] struct: stop trying to map empty nil pointers, correctly map most slice pointers --- struct.go | 126 +++++++++++++++++++++++++++++++------------------ struct_test.go | 115 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+), 46 deletions(-) diff --git a/struct.go b/struct.go index a486b2f..54a35ba 100644 --- a/struct.go +++ b/struct.go @@ -19,6 +19,7 @@ import ( "errors" "fmt" "reflect" + "strconv" "strings" "time" "unicode" @@ -77,8 +78,15 @@ func parseDelim(actual string) string { var reflectTime = reflect.TypeOf(time.Now()).Kind() +type typeStatus struct { + val reflect.Value + isPtr bool + addInvalid bool + returnOnInvalid bool +} + // setSliceWithProperType sets proper values to slice based on its type. -func setSliceWithProperType(key *Key, field reflect.Value, delim string, allowShadow, isStrict bool) error { +func setSliceWithProperType(key *Key, field reflect.Value, delim string, allowShadow, isStrict bool) (err error) { var strs []string if allowShadow { strs = key.StringsWithShadows(delim) @@ -88,57 +96,81 @@ func setSliceWithProperType(key *Key, field reflect.Value, delim string, allowSh numVals := len(strs) if numVals == 0 { - return nil + return } - var vals interface{} - var err error + status := typeStatus{addInvalid: true} + elem := field.Type().Elem() + sliceType := elem.Kind() + switch sliceType { + case reflect.String: + field.Set(reflect.ValueOf(strs)) + return + case reflect.Ptr: + sliceType = elem.Elem().Kind() + status.isPtr = true + } - sliceOf := field.Type().Elem().Kind() - switch sliceOf { + var setter func(str string) (reflect.Value, error) + switch sliceType { case reflect.String: - vals = strs + setter = func(str string) (reflect.Value, error) { + return reflect.ValueOf(&str), nil + } case reflect.Int: - vals, err = key.parseInts(strs, true, false) + setter = func(str string) (reflect.Value, error) { + parsed, err := strconv.ParseInt(str, 0, 64) + val := int(parsed) + return reflect.ValueOf(&val), err + } case reflect.Int64: - vals, err = key.parseInt64s(strs, true, false) + setter = func(str string) (reflect.Value, error) { + val, err := strconv.ParseInt(str, 0, 64) + return reflect.ValueOf(&val), err + } case reflect.Uint: - vals, err = key.parseUints(strs, true, false) + setter = func(str string) (reflect.Value, error) { + parsed, err := strconv.ParseUint(str, 0, 64) + val := uint(parsed) + return reflect.ValueOf(&val), err + } case reflect.Uint64: - vals, err = key.parseUint64s(strs, true, false) + setter = func(str string) (reflect.Value, error) { + val, err := strconv.ParseUint(str, 0, 64) + return reflect.ValueOf(&val), err + } case reflect.Float64: - vals, err = key.parseFloat64s(strs, true, false) + setter = func(str string) (reflect.Value, error) { + val, err := strconv.ParseFloat(str, 64) + return reflect.ValueOf(&val), err + } case reflect.Bool: - vals, err = key.parseBools(strs, true, false) + setter = func(str string) (reflect.Value, error) { + val, err := parseBool(str) + return reflect.ValueOf(&val), err + } case reflectTime: - vals, err = key.parseTimesFormat(time.RFC3339, strs, true, false) + setter = func(str string) (reflect.Value, error) { + val, err := time.Parse(time.RFC3339, str) + return reflect.ValueOf(&val), err + } default: - return fmt.Errorf("unsupported type '[]%s'", sliceOf) - } - if err != nil && isStrict { - return err + return fmt.Errorf("unsupported type '[]%s'", sliceType) } - slice := reflect.MakeSlice(field.Type(), numVals, numVals) + slice := reflect.MakeSlice(field.Type(), 0, numVals) for i := 0; i < numVals; i++ { - switch sliceOf { - case reflect.String: - slice.Index(i).Set(reflect.ValueOf(vals.([]string)[i])) - case reflect.Int: - slice.Index(i).Set(reflect.ValueOf(vals.([]int)[i])) - case reflect.Int64: - slice.Index(i).Set(reflect.ValueOf(vals.([]int64)[i])) - case reflect.Uint: - slice.Index(i).Set(reflect.ValueOf(vals.([]uint)[i])) - case reflect.Uint64: - slice.Index(i).Set(reflect.ValueOf(vals.([]uint64)[i])) - case reflect.Float64: - slice.Index(i).Set(reflect.ValueOf(vals.([]float64)[i])) - case reflect.Bool: - slice.Index(i).Set(reflect.ValueOf(vals.([]bool)[i])) - case reflectTime: - slice.Index(i).Set(reflect.ValueOf(vals.([]time.Time)[i])) + if status.val, err = setter(strs[i]); err != nil { + if status.returnOnInvalid { + return + } else if !status.addInvalid { + continue + } } + if !status.isPtr { + status.val = status.val.Elem() + } + slice = reflect.Append(slice, status.val) } field.Set(slice) return nil @@ -278,10 +310,15 @@ func parseTagOptions(tag string) (rawName string, omitEmpty bool, allowShadow bo // mapToField maps the given value to the matching field of the given section. // The sectionIndex is the index (if non unique sections are enabled) to which the value should be added. func (s *Section) mapToField(val reflect.Value, isStrict bool, sectionIndex int, sectionName string) error { + typ := val.Type() + // Early normalization of structs if val.Kind() == reflect.Ptr { + typ = val.Type().Elem() + if val.IsNil() { + val.Set(reflect.New(typ)) + } val = val.Elem() } - typ := val.Type() for i := 0; i < typ.NumField(); i++ { field := val.Field(i) @@ -324,11 +361,6 @@ func (s *Section) mapToField(val reflect.Value, isStrict bool, sectionIndex int, if len(secs) <= sectionIndex { return fmt.Errorf("there are not enough sections (%d <= %d) for the field %q", len(secs), sectionIndex, fieldName) } - // Only set the field to non-nil struct value if we have a section for it. - // Otherwise, we end up with a non-nil struct ptr even though there is no data. - if isStructPtr && field.IsNil() { - field.Set(reflect.New(tpField.Type.Elem())) - } if err = secs[sectionIndex].mapToField(field, isStrict, sectionIndex, fieldName); err != nil { return fmt.Errorf("map to field %q: %v", fieldName, err) } @@ -367,12 +399,11 @@ func (s *Section) mapToSlice(secName string, val reflect.Value, isStrict bool) ( typ := val.Type().Elem() for i, sec := range secs { - elem := reflect.New(typ) + val = reflect.Append(val, reflect.Zero(typ)) + elem := val.Index(val.Len() - 1) if err = sec.mapToField(elem, isStrict, i, sec.name); err != nil { return reflect.Value{}, fmt.Errorf("map to field from section %q: %v", secName, err) } - - val = reflect.Append(val, elem.Elem()) } return val, nil } @@ -381,7 +412,10 @@ func (s *Section) mapToSlice(secName string, val reflect.Value, isStrict bool) ( func (s *Section) mapTo(v interface{}, isStrict bool) error { typ := reflect.TypeOf(v) val := reflect.ValueOf(v) - if typ.Kind() == reflect.Ptr { + isPtr := typ.Kind() == reflect.Ptr + if isPtr && val.IsNil() { + return fmt.Errorf("cannot decode to nil value of %q", typ) + } else if isPtr { typ = typ.Elem() val = val.Elem() } else { diff --git a/struct_test.go b/struct_test.go index b51243a..8a20096 100644 --- a/struct_test.go +++ b/struct_test.go @@ -91,6 +91,16 @@ type testNonUniqueSectionsStruct struct { Interface testInterface Peer []testPeer `ini:",nonunique"` } +type testPeerPtr struct { + PublicKey string + PresharedKey string + AllowedIPs []*string `delim:","` +} + +type testNonUniqueSectionsPtr struct { + Interface testInterface + Peer []*testPeerPtr `ini:",nonunique"` +} type BaseStruct struct { Base bool @@ -291,6 +301,15 @@ func Test_MapToStruct(t *testing.T) { assert.Error(t, f.MapTo(testStruct{})) }) + t.Run("map to nil target pointer", func(t *testing.T) { + f, err := Load([]byte(confDataStruct)) + require.NoError(t, err) + require.NotNil(t, f) + + var ts *testStruct // nil pointer + assert.Error(t, f.MapTo(ts)) + }) + t.Run("map to unsupported type", func(t *testing.T) { f, err := Load([]byte(confDataStruct)) require.NoError(t, err) @@ -481,6 +500,102 @@ FieldInSection = 6 }) }) } +func Test_MapToStructNonUniquePtr(t *testing.T) { + t.Run("map to struct non unique", func(t *testing.T) { + t.Run("map file to struct non unique", func(t *testing.T) { + f, err := LoadSources(LoadOptions{AllowNonUniqueSections: true}, []byte(confNonUniqueSectionDataStruct)) + require.NoError(t, err) + ts := new(testNonUniqueSectionsPtr) + + assert.NoError(t, f.MapTo(ts)) + + assert.Equal(t, "10.2.0.1/24", ts.Interface.Address) + assert.Equal(t, 34777, ts.Interface.ListenPort) + assert.Equal(t, "privServerKey", ts.Interface.PrivateKey) + + assert.Equal(t, "pubClientKey", ts.Peer[0].PublicKey) + assert.Equal(t, "psKey", ts.Peer[0].PresharedKey) + assert.Equal(t, "10.2.0.2/32", *ts.Peer[0].AllowedIPs[0]) + assert.Equal(t, "fd00:2::2/128", *ts.Peer[0].AllowedIPs[1]) + + assert.Equal(t, "pubClientKey2", ts.Peer[1].PublicKey) + assert.Equal(t, "psKey2", ts.Peer[1].PresharedKey) + assert.Equal(t, "10.2.0.3/32", *ts.Peer[1].AllowedIPs[0]) + assert.Equal(t, "fd00:2::3/128", *ts.Peer[1].AllowedIPs[1]) + }) + + t.Run("map non unique section to struct", func(t *testing.T) { + newPeer := new(testPeerPtr) + newPeerSlice := make([]testPeerPtr, 0) + + f, err := LoadSources(LoadOptions{AllowNonUniqueSections: true}, []byte(confNonUniqueSectionDataStruct)) + require.NoError(t, err) + + // try only first one + assert.NoError(t, f.Section("Peer").MapTo(newPeer)) + assert.Equal(t, "pubClientKey", newPeer.PublicKey) + assert.Equal(t, "psKey", newPeer.PresharedKey) + assert.Equal(t, "10.2.0.2/32", *newPeer.AllowedIPs[0]) + assert.Equal(t, "fd00:2::2/128", *newPeer.AllowedIPs[1]) + + // try all + assert.NoError(t, f.Section("Peer").MapTo(&newPeerSlice)) + assert.Equal(t, "pubClientKey", newPeerSlice[0].PublicKey) + assert.Equal(t, "psKey", newPeerSlice[0].PresharedKey) + assert.Equal(t, "10.2.0.2/32", *newPeerSlice[0].AllowedIPs[0]) + assert.Equal(t, "fd00:2::2/128", *newPeerSlice[0].AllowedIPs[1]) + + assert.Equal(t, "pubClientKey2", newPeerSlice[1].PublicKey) + assert.Equal(t, "psKey2", newPeerSlice[1].PresharedKey) + assert.Equal(t, "10.2.0.3/32", *newPeerSlice[1].AllowedIPs[0]) + assert.Equal(t, "fd00:2::3/128", *newPeerSlice[1].AllowedIPs[1]) + }) + + t.Run("map non unique sections with subsections to struct", func(t *testing.T) { + iniFile, err := LoadSources(LoadOptions{AllowNonUniqueSections: true}, strings.NewReader(` +[Section] +FieldInSubSection = 1 +FieldInSubSection2 = 2 +FieldInSection = 3 + +[Section] +FieldInSubSection = 4 +FieldInSubSection2 = 5 +FieldInSection = 6 +`)) + require.NoError(t, err) + + type SubSection struct { + FieldInSubSection string `ini:"FieldInSubSection"` + } + type SubSection2 struct { + FieldInSubSection2 string `ini:"FieldInSubSection2"` + } + + type Section struct { + SubSection `ini:"Section"` + SubSection2 `ini:"Section"` + FieldInSection string `ini:"FieldInSection"` + } + + type File struct { + Sections []*Section `ini:"Section,nonunique"` + } + + f := new(File) + err = iniFile.MapTo(f) + require.NoError(t, err) + + assert.Equal(t, "1", f.Sections[0].FieldInSubSection) + assert.Equal(t, "2", f.Sections[0].FieldInSubSection2) + assert.Equal(t, "3", f.Sections[0].FieldInSection) + + assert.Equal(t, "4", f.Sections[1].FieldInSubSection) + assert.Equal(t, "5", f.Sections[1].FieldInSubSection2) + assert.Equal(t, "6", f.Sections[1].FieldInSection) + }) + }) +} func Test_ReflectFromStruct(t *testing.T) { t.Run("reflect from struct", func(t *testing.T) {