Skip to content

Commit 31c2c6c

Browse files
committed
Fix unmarshaler offset error in struct fields
When custom unmarshalers decoded container types in struct fields, the reflection decoder would fail with "no next offset available" when trying to advance to the next field.
1 parent a9dfa5e commit 31c2c6c

File tree

4 files changed

+89
-10
lines changed

4 files changed

+89
-10
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changes
22

3+
## 2.0.0-beta.8 - 2025-07-15
4+
5+
- Fixed "no next offset available" error that occurred when using custom
6+
unmarshalers that decode container types (maps, slices) in struct fields.
7+
The reflection decoder now correctly calculates field positions when
8+
advancing to the next field after custom unmarshaling.
9+
310
## 2.0.0-beta.7 - 2025-07-07
411

512
* Update capitalization of "uint" in `ReadUInt*` to match `KindUint*` as well

internal/decoder/decoder.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package decoder
22

33
import (
4-
"errors"
54
"fmt"
65
"iter"
76

@@ -370,13 +369,6 @@ func (d *Decoder) setNextOffset(offset uint) {
370369
}
371370
}
372371

373-
func (d *Decoder) getNextOffset() (uint, error) {
374-
if !d.hasNextOffset {
375-
return 0, errors.New("no next offset available")
376-
}
377-
return d.nextOffset, nil
378-
}
379-
380372
func unexpectedKindErr(expectedKind, actualKind Kind) error {
381373
return fmt.Errorf("unexpected kind %d, expected %d", actualKind, expectedKind)
382374
}

internal/decoder/nested_unmarshaler_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package decoder
33
import (
44
"testing"
55

6+
"github.com/stretchr/testify/assert"
67
"github.com/stretchr/testify/require"
78
)
89

@@ -220,3 +221,82 @@ func TestNestedUnmarshalerInMap(t *testing.T) {
220221
require.Equal(t, "map:value2", result["key2"].Data)
221222
})
222223
}
224+
225+
// testMapIterator uses ReadMap() iterator to simulate mmdbtype.Map behavior.
226+
type testMapIterator struct {
227+
Values map[string]string
228+
custom bool
229+
}
230+
231+
func (m *testMapIterator) UnmarshalMaxMindDB(d *Decoder) error {
232+
m.custom = true
233+
iter, size, err := d.ReadMap()
234+
if err != nil {
235+
return err
236+
}
237+
238+
m.Values = make(map[string]string, size)
239+
for key, iterErr := range iter {
240+
if iterErr != nil {
241+
return iterErr
242+
}
243+
244+
// Read the value as a string
245+
value, err := d.ReadString()
246+
if err != nil {
247+
return err
248+
}
249+
250+
m.Values[string(key)] = value
251+
}
252+
return nil
253+
}
254+
255+
// TestCustomUnmarshalerWithIterator tests that custom unmarshalers using iterators
256+
// work correctly in struct fields. This reproduces the original "no next offset available"
257+
// issue that occurred when mmdbtype.Map was used in structs.
258+
func TestCustomUnmarshalerWithIterator(t *testing.T) {
259+
type Record struct {
260+
Name string
261+
Location testMapIterator // This field uses ReadMap() iterator
262+
Country string
263+
}
264+
265+
data := []byte{
266+
// Map with 3 items
267+
0xe3,
268+
// Key "Name"
269+
0x44, 'N', 'a', 'm', 'e',
270+
// Value "Test" (string)
271+
0x44, 'T', 'e', 's', 't',
272+
// Key "Location"
273+
0x48, 'L', 'o', 'c', 'a', 't', 'i', 'o', 'n',
274+
// Value: Map with 2 items (latitude and longitude)
275+
0xe2,
276+
// Key "lat"
277+
0x43, 'l', 'a', 't',
278+
// Value "40.7"
279+
0x44, '4', '0', '.', '7',
280+
// Key "lng"
281+
0x43, 'l', 'n', 'g',
282+
// Value "-74.0"
283+
0x45, '-', '7', '4', '.', '0',
284+
// Key "Country"
285+
0x47, 'C', 'o', 'u', 'n', 't', 'r', 'y',
286+
// Value "US"
287+
0x42, 'U', 'S',
288+
}
289+
290+
d := New(data)
291+
var result Record
292+
293+
err := d.Decode(0, &result)
294+
require.NoError(t, err)
295+
296+
require.Equal(t, "Test", result.Name)
297+
assert.True(t, result.Location.custom, "Custom unmarshaler should be called")
298+
assert.Len(t, result.Location.Values, 2)
299+
assert.Equal(t, "40.7", result.Location.Values["lat"])
300+
assert.Equal(t, "-74.0", result.Location.Values["lng"])
301+
assert.Equal(t, "US", result.Country)
302+
}

internal/decoder/reflection.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func (d *ReflectionDecoder) Decode(offset uint, v any) error {
6464
return mmdberrors.WrapWithContext(err, offset, nil)
6565
}
6666

67-
// DecodePath decodes the data value at offset and stores the value assocated
67+
// DecodePath decodes the data value at offset and stores the value associated
6868
// with the path in the value pointed at by v.
6969
func (d *ReflectionDecoder) DecodePath(
7070
offset uint,
@@ -300,7 +300,7 @@ func (d *ReflectionDecoder) decodeValue(
300300
if err := unmarshaler.UnmarshalMaxMindDB(decoder); err != nil {
301301
return 0, err
302302
}
303-
return decoder.getNextOffset()
303+
return d.nextValueOffset(offset, 1)
304304
}
305305
}
306306

0 commit comments

Comments
 (0)