From 549210b74c10b13891adf2a5149c9a7bc7ea9336 Mon Sep 17 00:00:00 2001 From: Ian Lopshire Date: Thu, 8 Feb 2024 12:54:02 -0500 Subject: [PATCH] Fix bug and possible panic when decoding nested structs --- decode.go | 29 +++++++++++++----- decode_test.go | 80 +++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 97 insertions(+), 12 deletions(-) diff --git a/decode.go b/decode.go index a565e66..c0d4f67 100644 --- a/decode.go +++ b/decode.go @@ -196,7 +196,7 @@ func (d *Decoder) readLine(v reflect.Value) (err error, ok bool) { return valueSetter(v, rawValue), true } -func rawValueFromLine(value rawValue, startPos, endPos int, format format) rawValue { +func rawValueFromLine(value rawValue, startPos, endPos int, format format) (rawValue, error) { var trimFunc func(string) string switch format.alignment { @@ -218,7 +218,7 @@ func rawValueFromLine(value rawValue, startPos, endPos int, format format) rawVa if value.codepointIndices != nil { if len(value.codepointIndices) == 0 || startPos > len(value.codepointIndices) { - return rawValue{data: ""} + return rawValue{data: ""}, nil } var relevantIndices []int var lineData string @@ -229,20 +229,30 @@ func rawValueFromLine(value rawValue, startPos, endPos int, format format) rawVa relevantIndices = value.codepointIndices[startPos-1 : endPos] lineData = value.data[relevantIndices[0]:value.codepointIndices[endPos]] } + + trimmed := trimFunc(lineData) + + // If the value was trimmed or didn't start at the first codepoint, the indices + // will be incorrect. Create a new rawValue to get the correct indices. + if len(trimmed) != len(lineData) || len(relevantIndices) == 0 { + return newRawValue(trimmed, true) + } + return rawValue{ - data: trimFunc(lineData), + data: lineData, codepointIndices: relevantIndices, - } + }, nil + } else { if len(value.data) == 0 || startPos > len(value.data) { - return rawValue{data: ""} + return rawValue{data: ""}, nil } if endPos > len(value.data) { endPos = len(value.data) } return rawValue{ data: trimFunc(value.data[startPos-1 : endPos]), - } + }, nil } } @@ -288,9 +298,12 @@ func structSetter(t reflect.Type) valueSetter { if !fieldSpec.ok { continue } - rawValue := rawValueFromLine(raw, fieldSpec.startPos, fieldSpec.endPos, fieldSpec.format) - err := fieldSpec.setter(v.Field(i), rawValue) + + rawValue, err := rawValueFromLine(raw, fieldSpec.startPos, fieldSpec.endPos, fieldSpec.format) if err != nil { + return err + } + if err := fieldSpec.setter(v.Field(i), rawValue); err != nil { sf := t.Field(i) return &UnmarshalTypeError{raw.data, sf.Type, t.Name(), sf.Name, err} } diff --git a/decode_test.go b/decode_test.go index 4afc2f3..1f3764c 100644 --- a/decode_test.go +++ b/decode_test.go @@ -40,10 +40,10 @@ func ExampleUnmarshal() { fmt.Printf("%+v\n", people[2]) fmt.Printf("%+v\n", people[3]) // Output: - //{ID:1 FirstName:Ian LastName:Lopshire Grade:99.5 Age:20 Alive:false Github:false} - //{ID:2 FirstName:John LastName:Doe Grade:89.5 Age:21 Alive:true Github:true} - //{ID:3 FirstName:Jane LastName:Doe Grade:79.5 Age:22 Alive:false Github:false} - //{ID:4 FirstName:Ann LastName:Carraway Grade:79.59 Age:23 Alive:false Github:true} + // {ID:1 FirstName:Ian LastName:Lopshire Grade:99.5 Age:20 Alive:false Github:false} + // {ID:2 FirstName:John LastName:Doe Grade:89.5 Age:21 Alive:true Github:true} + // {ID:3 FirstName:Jane LastName:Doe Grade:79.5 Age:22 Alive:false Github:false} + // {ID:4 FirstName:Ann LastName:Carraway Grade:79.59 Age:23 Alive:false Github:true} } func TestUnmarshal(t *testing.T) { @@ -539,3 +539,75 @@ func TestLineSeparator(t *testing.T) { }) } } + +func TestDecode_Nested(t *testing.T) { + type Nested struct { + First string `fixed:"1,3"` + Second string `fixed:"4,6"` + } + + type Test struct { + Nested Nested `fixed:"1,6,none"` + } + + for _, tt := range []struct { + name string + raw []byte + + expected Test + }{ + { + name: "ascii no trimming needed", + raw: []byte("123ABC\n"), + expected: Test{ + Nested: Nested{ + First: "123", + Second: "ABC", + }, + }, + }, + { + name: "ascii with trimmed values", + raw: []byte(" 12BC\n"), + expected: Test{ + Nested: Nested{ + First: "12", + Second: "BC", + }, + }, + }, + { + name: "Multi-byte no trimming needed", + raw: []byte("12☃☃BC\n"), + expected: Test{ + Nested: Nested{ + First: "12☃", + Second: "☃BC", + }, + }, + }, + { + name: "Multi-byte with trimmed values", + raw: []byte(" ☃2B☃\n"), + expected: Test{ + Nested: Nested{ + First: "☃2", + Second: "B☃", + }, + }, + }, + } { + t.Run(tt.name, func(t *testing.T) { + d := NewDecoder(bytes.NewReader(tt.raw)) + d.SetUseCodepointIndices(true) + var s Test + err := d.Decode(&s) + if err != nil { + t.Errorf("Unexpected err: %v", err) + } + if !reflect.DeepEqual(tt.expected, s) { + t.Errorf("Decode(%v) want %v, have %v", tt.raw, tt.expected, s) + } + }) + } +}