Skip to content

Commit 517adc2

Browse files
authored
Merge pull request #203 from oschwald/greg/cleanups
Misc cleanup
2 parents f779ab8 + 201dd7d commit 517adc2

File tree

15 files changed

+189
-195
lines changed

15 files changed

+189
-195
lines changed

CHANGELOG.md

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

3+
## 2.2.0 - TBD
4+
5+
- An error is returned when a `maxminddb` struct tag is clearly invalid (non
6+
UTF-8) instead of silently ignoring validation failures.
7+
38
## 2.1.1 - 2025-11-26
49

510
- Fixed `runtime.AddCleanup` misuse that prevented the memory-mapped file from

internal/decoder/data_decoder.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ type DataDecoder struct {
113113
const (
114114
// This is the value used in libmaxminddb.
115115
maximumDataStructureDepth = 512
116+
pointerBase2 = 2048
117+
pointerBase3 = 526336
116118
)
117119

118120
// NewDataDecoder creates a [DataDecoder].
@@ -240,9 +242,9 @@ func (d *DataDecoder) decodePointer(
240242
case 1, 4:
241243
pointerValueOffset = 0
242244
case 2:
243-
pointerValueOffset = 2048
245+
pointerValueOffset = pointerBase2
244246
case 3:
245-
pointerValueOffset = 526336
247+
pointerValueOffset = pointerBase3
246248
default:
247249
return 0, 0, mmdberrors.NewInvalidDatabaseError("invalid pointer size: %d", pointerSize)
248250
}

internal/decoder/decoder.go

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ type Decoder struct {
1414
d DataDecoder
1515
offset uint
1616
nextOffset uint
17-
opts decoderOptions
1817
hasNextOffset bool
1918
}
2019

2120
type decoderOptions struct {
22-
// Reserved for future options
21+
// Intentionally empty for now. DecoderOption callbacks are still invoked so
22+
// adding options in a future release is non-breaking.
2323
}
2424

2525
// DecoderOption configures a Decoder.
@@ -34,13 +34,10 @@ func NewDecoder(d DataDecoder, offset uint, options ...DecoderOption) *Decoder {
3434
option(&opts)
3535
}
3636

37-
decoder := &Decoder{
37+
return &Decoder{
3838
d: d,
3939
offset: offset,
40-
opts: opts,
4140
}
42-
43-
return decoder
4441
}
4542

4643
// ReadBool reads the value pointed by the decoder as a bool.
@@ -124,7 +121,7 @@ func (d *Decoder) ReadFloat64() (float64, error) {
124121
return value, nil
125122
}
126123

127-
// ReadInt32 reads the value pointed by the decoder as a int32.
124+
// ReadInt32 reads the value pointed by the decoder as an int32.
128125
//
129126
// Returns an error if the database is malformed or if the pointed value is not an int32.
130127
func (d *Decoder) ReadInt32() (int32, error) {
@@ -144,7 +141,7 @@ func (d *Decoder) ReadInt32() (int32, error) {
144141

145142
// ReadUint16 reads the value pointed by the decoder as a uint16.
146143
//
147-
// Returns an error if the database is malformed or if the pointed value is not an uint16.
144+
// Returns an error if the database is malformed or if the pointed value is not a uint16.
148145
func (d *Decoder) ReadUint16() (uint16, error) {
149146
size, offset, err := d.decodeCtrlDataAndFollow(KindUint16)
150147
if err != nil {
@@ -162,7 +159,7 @@ func (d *Decoder) ReadUint16() (uint16, error) {
162159

163160
// ReadUint32 reads the value pointed by the decoder as a uint32.
164161
//
165-
// Returns an error if the database is malformed or if the pointed value is not an uint32.
162+
// Returns an error if the database is malformed or if the pointed value is not a uint32.
166163
func (d *Decoder) ReadUint32() (uint32, error) {
167164
size, offset, err := d.decodeCtrlDataAndFollow(KindUint32)
168165
if err != nil {
@@ -180,7 +177,7 @@ func (d *Decoder) ReadUint32() (uint32, error) {
180177

181178
// ReadUint64 reads the value pointed by the decoder as a uint64.
182179
//
183-
// Returns an error if the database is malformed or if the pointed value is not an uint64.
180+
// Returns an error if the database is malformed or if the pointed value is not a uint64.
184181
func (d *Decoder) ReadUint64() (uint64, error) {
185182
size, offset, err := d.decodeCtrlDataAndFollow(KindUint64)
186183
if err != nil {
@@ -198,7 +195,7 @@ func (d *Decoder) ReadUint64() (uint64, error) {
198195

199196
// ReadUint128 reads the value pointed by the decoder as a uint128.
200197
//
201-
// Returns an error if the database is malformed or if the pointed value is not an uint128.
198+
// Returns an error if the database is malformed or if the pointed value is not a uint128.
202199
func (d *Decoder) ReadUint128() (hi, lo uint64, err error) {
203200
size, offset, err := d.decodeCtrlDataAndFollow(KindUint128)
204201
if err != nil {

internal/decoder/decoder_test.go

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ func TestDecodeUint16(t *testing.T) {
245245
"a1ff": uint16(255),
246246
"a201f4": uint16(500),
247247
"a22a78": uint16(10872),
248-
"a2ffff": uint16(65535), // [cite: 88] - Note: reflection test uses uint64 expected value
248+
"a2ffff": uint16(65535),
249249
}
250250

251251
for hexStr, expected := range tests {
@@ -337,16 +337,12 @@ func TestDecodeUint128(t *testing.T) {
337337
}
338338
}
339339

340-
// TestPointers requires a specific data file and structure.
341340
func TestPointersInDecoder(t *testing.T) {
342-
// This test requires the 'maps-with-pointers.raw' file used in reflection_test [cite: 92]
343-
// It demonstrates how to handle pointers using the basic Decoder.
344-
bytes, err := os.ReadFile(testFile("maps-with-pointers.raw")) // [cite: 92]
341+
bytes, err := os.ReadFile(testFile("maps-with-pointers.raw"))
345342
require.NoError(t, err)
346343
dd := NewDataDecoder(bytes)
347344

348345
expected := map[uint]map[string]string{
349-
// Offsets and expected values from reflection_test.go [cite: 92]
350346
0: {"long_key": "long_value1"},
351347
22: {"long_key": "long_value2"},
352348
37: {"long_key2": "long_value1"},
@@ -358,12 +354,11 @@ func TestPointersInDecoder(t *testing.T) {
358354
for startOffset, expectedValue := range expected {
359355
t.Run(fmt.Sprintf("Offset_%d", startOffset), func(t *testing.T) {
360356
decoder := NewDecoder(dd, startOffset) // Start at the specific offset
361-
actualValue := make(map[string]string)
362357

363358
// Expecting a map at the target offset (may be behind a pointer)
364359
mapIter, size, err := decoder.ReadMap()
365360
require.NoError(t, err, "ReadMap failed")
366-
_ = size // Use size if needed for pre-allocation
361+
actualValue := make(map[string]string, int(size))
367362
for keyBytes, errIter := range mapIter {
368363
require.NoError(t, errIter)
369364
key := string(keyBytes)
@@ -487,28 +482,18 @@ func TestPeekKind(t *testing.T) {
487482
// TestPeekKindWithPointer tests that PeekKind correctly follows pointers
488483
// to get the actual kind of the pointed-to value.
489484
func TestPeekKindWithPointer(t *testing.T) {
490-
// Create a buffer with a pointer that points to a string
491-
// This is a simplified test - in real MMDB files pointers are more complex
485+
// Create a buffer with a pointer at offset 0 and a string at offset 5.
492486
buffer := []byte{
493-
// Pointer (TypePointer=1, size/pointer encoding)
494-
0x20, 0x05, // Simple pointer to offset 5
495-
// Target string at offset 5 (but we'll put it at offset 2 for this test)
496-
0x44, 't', 'e', 's', 't', // String "test"
487+
0x20, 0x05,
488+
0x00, 0x00, 0x00,
489+
0x44, 't', 'e', 's', 't',
497490
}
498491

499492
decoder := NewDecoder(NewDataDecoder(buffer), 0)
500493

501-
// PeekKind should follow the pointer and return KindString
502494
actualType, err := decoder.PeekKind()
503495
require.NoError(t, err, "PeekKind with pointer failed")
504-
505-
// Note: This test may need adjustment based on actual pointer encoding
506-
// The important thing is that PeekKind follows pointers
507-
if actualType != KindPointer {
508-
// If the implementation follows pointers completely, it should return the target kind
509-
// If it just returns KindPointer, that's also acceptable behavior
510-
t.Logf("PeekKind returned %d (this may be expected behavior)", actualType)
511-
}
496+
require.Equal(t, KindString, actualType)
512497
}
513498

514499
// ExampleDecoder_PeekKind demonstrates how to use PeekKind for
@@ -556,12 +541,21 @@ func ExampleDecoder_PeekKind() {
556541
func TestDecoderOptions(t *testing.T) {
557542
buffer := []byte{0x44, 't', 'e', 's', 't'} // String "test"
558543
dd := NewDataDecoder(buffer)
544+
optionCalled := false
545+
option := func(*decoderOptions) {
546+
optionCalled = true
547+
}
559548

560-
// Test that options infrastructure works (even with no current options)
549+
// Test that options infrastructure works (even with no current options).
561550
decoder1 := NewDecoder(dd, 0)
562551
require.NotNil(t, decoder1)
563552

564-
// Test that passing empty options slice works
553+
// Test that passing options invokes each option callback.
554+
decoderWithOption := NewDecoder(dd, 0, option)
555+
require.NotNil(t, decoderWithOption)
556+
require.True(t, optionCalled)
557+
558+
// Test that passing empty options slice works.
565559
decoder2 := NewDecoder(dd, 0)
566560
require.NotNil(t, decoder2)
567561
}

internal/decoder/error_context.go

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,6 @@
11
package decoder
22

3-
import (
4-
"github.com/oschwald/maxminddb-golang/v2/internal/mmdberrors"
5-
)
6-
7-
// errorContext provides zero-allocation error context tracking for Decoder.
8-
// This is only used when an error occurs, ensuring no performance impact
9-
// on the happy path.
10-
type errorContext struct {
11-
path *mmdberrors.PathBuilder // Only allocated when needed
12-
}
13-
14-
// BuildPath implements mmdberrors.ErrorContextTracker.
15-
// This is only called when an error occurs, so allocation is acceptable.
16-
func (e *errorContext) BuildPath() string {
17-
if e.path == nil {
18-
return "" // No path tracking enabled
19-
}
20-
return e.path.Build()
21-
}
3+
import "github.com/oschwald/maxminddb-golang/v2/internal/mmdberrors"
224

235
// wrapError wraps an error with context information when an error occurs.
246
// Zero allocation on happy path - only allocates when error != nil.
@@ -38,12 +20,3 @@ func (*Decoder) wrapErrorAtOffset(err error, offset uint) error {
3820
}
3921
return mmdberrors.WrapWithContext(err, offset, nil)
4022
}
41-
42-
// Example of how to integrate into existing decoder methods:
43-
// Instead of:
44-
// return mmdberrors.NewInvalidDatabaseError("message")
45-
// Use:
46-
// return d.wrapError(mmdberrors.NewInvalidDatabaseError("message"))
47-
//
48-
// This adds zero overhead when no error occurs, but provides rich context
49-
// when errors do happen.

internal/decoder/error_context_test.go

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func TestWrapError_ZeroAllocationHappyPath(t *testing.T) {
1818
err := decoder.wrapError(nil)
1919
require.NoError(t, err)
2020

21-
// DataDecoder should always have path tracking enabled
21+
// Decoder should be initialized
2222
require.NotNil(t, decoder.d)
2323
}
2424

@@ -48,22 +48,18 @@ func TestPathBuilder(t *testing.T) {
4848
// Test basic path building
4949
require.Equal(t, "/", builder.Build())
5050

51-
builder.PushMap("city")
51+
builder.PrependMap("city")
5252
require.Equal(t, "/city", builder.Build())
5353

54-
builder.PushMap("names")
55-
require.Equal(t, "/city/names", builder.Build())
56-
57-
builder.PushSlice(0)
58-
require.Equal(t, "/city/names/0", builder.Build())
54+
builder.PrependMap("names")
55+
require.Equal(t, "/names/city", builder.Build())
5956

60-
// Test pop
61-
builder.Pop()
57+
builder = mmdberrors.NewPathBuilder()
58+
builder.ParseAndExtend("/city/names")
6259
require.Equal(t, "/city/names", builder.Build())
6360

64-
// Test reset
65-
builder.Reset()
66-
require.Equal(t, "/", builder.Build())
61+
builder.PrependSlice(0)
62+
require.Equal(t, "/0/city/names", builder.Build())
6763
}
6864

6965
// Benchmark to verify zero allocation on happy path.
@@ -104,15 +100,14 @@ func BenchmarkWrapError_ErrorPath(b *testing.B) {
104100
func ExampleContextualError() {
105101
// This would be internal to the decoder, shown for illustration
106102
builder := mmdberrors.NewPathBuilder()
107-
builder.PushMap("city")
108-
builder.PushMap("names")
109-
builder.PushMap("en")
103+
builder.PrependMap("en")
104+
builder.PrependMap("names")
105+
builder.PrependMap("city")
110106

111107
// Simulate an error with context
112108
originalErr := mmdberrors.NewInvalidDatabaseError("string too long")
113109

114-
contextTracker := &errorContext{path: builder}
115-
wrappedErr := mmdberrors.WrapWithContext(originalErr, 1234, contextTracker)
110+
wrappedErr := mmdberrors.WrapWithContext(originalErr, 1234, builder)
116111

117112
fmt.Println(wrappedErr.Error())
118113
// Output: at offset 1234, path /city/names/en: string too long
@@ -211,13 +206,9 @@ func TestContextualErrorIntegration(t *testing.T) {
211206
err := rd.Decode(0, &result)
212207
require.Error(t, err)
213208

214-
// Debug: print the actual error and path
215-
t.Logf("Error: %v", err)
216-
217209
// Should get a wrapped error with slice index in path
218210
var contextErr mmdberrors.ContextualError
219211
require.ErrorAs(t, err, &contextErr)
220-
t.Logf("Path: %s", contextErr.Path)
221212

222213
// Verify we get the exact path with correct order
223214
require.Equal(t, "/list/0/name", contextErr.Path)

0 commit comments

Comments
 (0)