Skip to content

Commit a0df29f

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 a0df29f

File tree

4 files changed

+88
-10
lines changed

4 files changed

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

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)