Skip to content

Commit 32a7766

Browse files
author
Johnny Graettinger
committed
Add Reader.LookupOffset & Decode
Expose database offsets, and decoding records at those offsets, as a first-class concept via LookupOffset & Decode. Allow decoded records to indirect nested structures by introducing special handling for the uintptr type: fields with this type are decoded by storing the database offset of the indirected record. Reflection is expensive, many database records ("continent", "country", etc) are embarrassingly cacheable, and database offsets make for excellent caching keys. These methods allow clients to trivially identify and re-use structures which have previously been extracted.
1 parent 7992a44 commit 32a7766

File tree

4 files changed

+129
-59
lines changed

4 files changed

+129
-59
lines changed

decoder.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@ const (
3535

3636
func (d *decoder) decode(offset uint, result reflect.Value) (uint, error) {
3737
typeNum, size, newOffset := d.decodeCtrlData(offset)
38-
return d.decodeFromType(typeNum, size, newOffset, result)
38+
39+
if typeNum != _Pointer && result.Kind() == reflect.Uintptr {
40+
result.Set(reflect.ValueOf(uintptr(offset)))
41+
return d.nextValueOffset(offset, 1), nil
42+
} else {
43+
return d.decodeFromType(typeNum, size, newOffset, result)
44+
}
3945
}
4046

4147
func (d *decoder) decodeCtrlData(offset uint) (dataType, uint, uint) {

reader.go

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import (
88
"reflect"
99
)
1010

11-
const dataSectionSeparatorSize = 16
11+
const (
12+
// Returned by LookupOffset when a matched root record offset cannot be found.
13+
NotFound = ^uintptr(0)
14+
15+
dataSectionSeparatorSize = 16
16+
)
1217

1318
var metadataStartMarker = []byte("\xAB\xCD\xEFMaxMind.com")
1419

@@ -113,25 +118,48 @@ func (r *Reader) startNode() (uint, error) {
113118
// a uint64 database type must be decoded into a uint64 Go type). In the
114119
// future, this may be made more flexible.
115120
func (r *Reader) Lookup(ipAddress net.IP, result interface{}) error {
121+
if pointer, err := r.lookupPointer(ipAddress); pointer == 0 {
122+
return err
123+
} else {
124+
return r.retrieveData(pointer, result)
125+
}
126+
}
127+
128+
// LookupOffset maps an argument net.IP to corresponding root record offset
129+
// in the database. NotFound is returned if no such record is found.
130+
func (r *Reader) LookupOffset(ipAddress net.IP) (uintptr, error) {
131+
if pointer, err := r.lookupPointer(ipAddress); pointer == 0 {
132+
return NotFound, err
133+
} else {
134+
return r.resolveDataPointer(pointer)
135+
}
136+
}
137+
138+
// Decodes the record at |offset| into |result|.
139+
func (r *Reader) Decode(offset uintptr, result interface{}) error {
140+
rv := reflect.ValueOf(result)
141+
if rv.Kind() != reflect.Ptr || rv.IsNil() {
142+
return errors.New("result param must be a pointer")
143+
}
144+
145+
_, err := r.decoder.decode(uint(offset), reflect.ValueOf(result))
146+
return err
147+
}
148+
149+
func (r *Reader) lookupPointer(ipAddress net.IP) (uint, error) {
116150
if ipAddress == nil {
117-
return errors.New("ipAddress passed to Lookup cannot be nil")
151+
return 0, errors.New("ipAddress passed to Lookup cannot be nil")
118152
}
119153

120154
ipV4Address := ipAddress.To4()
121155
if ipV4Address != nil {
122156
ipAddress = ipV4Address
123157
}
124158
if len(ipAddress) == 16 && r.Metadata.IPVersion == 4 {
125-
return fmt.Errorf("error looking up '%s': you attempted to look up an IPv6 address in an IPv4-only database", ipAddress.String())
126-
}
127-
128-
pointer, err := r.findAddressInTree(ipAddress)
129-
130-
if pointer == 0 {
131-
return err
159+
return 0, fmt.Errorf("error looking up '%s': you attempted to look up an IPv6 address in an IPv4-only database", ipAddress.String())
132160
}
133161

134-
return r.retrieveData(pointer, result)
162+
return r.findAddressInTree(ipAddress)
135163
}
136164

137165
func (r *Reader) findAddressInTree(ipAddress net.IP) (uint, error) {
@@ -194,28 +222,18 @@ func (r *Reader) readNode(nodeNumber uint, index uint) (uint, error) {
194222
}
195223

196224
func (r *Reader) retrieveData(pointer uint, result interface{}) error {
197-
rv := reflect.ValueOf(result)
198-
if rv.Kind() != reflect.Ptr || rv.IsNil() {
199-
return errors.New("result param must be a pointer")
200-
}
201-
202-
offset, err := r.resolveDataPointer(pointer)
203-
if err != nil {
225+
if offset, err := r.resolveDataPointer(pointer); err != nil {
204226
return err
227+
} else {
228+
return r.Decode(offset, result)
205229
}
206-
207-
_, err = r.decoder.decode(offset, rv)
208-
return err
209230
}
210231

211-
func (r *Reader) resolveDataPointer(pointer uint) (uint, error) {
212-
nodeCount := r.Metadata.NodeCount
213-
214-
resolved := pointer - nodeCount - dataSectionSeparatorSize
232+
func (r *Reader) resolveDataPointer(pointer uint) (uintptr, error) {
233+
var resolved = uintptr(pointer - r.Metadata.NodeCount - dataSectionSeparatorSize)
215234

216-
if resolved > uint(len(r.buffer)) {
235+
if resolved > uintptr(len(r.buffer)) {
217236
return 0, newInvalidDatabaseError("the MaxMind DB file's search tree is corrupt")
218237
}
219-
220238
return resolved, nil
221239
}

reader_test.go

Lines changed: 77 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -116,43 +116,89 @@ type TestType struct {
116116

117117
func (s *MySuite) TestDecoder(c *C) {
118118
reader, err := Open("test-data/test-data/MaxMind-DB-test-decoder.mmdb")
119-
if err != nil {
120-
c.Logf("unexpected error while opening database: %v", err)
121-
c.Fail()
122-
}
119+
c.Assert(err, IsNil)
120+
121+
verify := func(result TestType) {
122+
c.Assert(result.Array, DeepEquals, []uint{uint(1), uint(2), uint(3)})
123+
c.Assert(result.Boolean, Equals, true)
124+
c.Assert(result.Bytes, DeepEquals, []byte{0x00, 0x00, 0x00, 0x2a})
125+
c.Assert(result.Double, Equals, 42.123456)
126+
c.Assert(result.Float, Equals, float32(1.1))
127+
c.Assert(result.Int32, Equals, int32(-268435456))
128+
129+
c.Assert(result.Map, DeepEquals,
130+
map[string]interface{}{
131+
"mapX": map[string]interface{}{
132+
"arrayX": []interface{}{uint64(7), uint64(8), uint64(9)},
133+
"utf8_stringX": "hello",
134+
}})
135+
136+
c.Assert(result.Uint16, Equals, uint16(100))
137+
c.Assert(result.Uint32, Equals, uint32(268435456))
138+
c.Assert(result.Uint64, Equals, uint64(1152921504606846976))
139+
c.Assert(result.Utf8String, Equals, "unicode! ☯ - ♫")
140+
bigInt := new(big.Int)
141+
bigInt.SetString("1329227995784915872903807060280344576", 10)
142+
c.Assert(&result.Uint128, DeepEquals, bigInt)
143+
}
144+
145+
{
146+
// Directly lookup and decode.
147+
var result TestType
148+
c.Assert(reader.Lookup(net.ParseIP("::1.1.1.0"), &result), IsNil)
149+
verify(result)
150+
}
151+
{
152+
// Lookup record offset, then Decode.
153+
var result TestType
154+
offset, err := reader.LookupOffset(net.ParseIP("::1.1.1.0"))
155+
c.Assert(err, IsNil)
156+
c.Assert(offset, Not(Equals), NotFound)
157+
158+
c.Assert(reader.Decode(offset, &result), IsNil)
159+
verify(result)
160+
}
161+
162+
c.Assert(reader.Close(), IsNil)
163+
}
123164

124-
var result TestType
125-
err = reader.Lookup(net.ParseIP("::1.1.1.0"), &result)
126-
if err != nil {
127-
c.Log(err)
128-
c.Fail()
129-
}
165+
func (s *MySuite) TestNestedOffsetDecode(c *C) {
166+
db, err := Open("test-data/test-data/GeoIP2-City-Test.mmdb")
167+
c.Assert(err, IsNil)
130168

131-
c.Assert(result.Array, DeepEquals, []uint{uint(1), uint(2), uint(3)})
132-
c.Assert(result.Boolean, Equals, true)
133-
c.Assert(result.Bytes, DeepEquals, []byte{0x00, 0x00, 0x00, 0x2a})
134-
c.Assert(result.Double, Equals, 42.123456)
135-
c.Assert(result.Float, Equals, float32(1.1))
136-
c.Assert(result.Int32, Equals, int32(-268435456))
169+
off, err := db.LookupOffset(net.ParseIP("81.2.69.142"))
170+
c.Assert(off, Not(Equals), NotFound)
171+
c.Check(err, IsNil)
137172

138-
c.Assert(result.Map, DeepEquals,
139-
map[string]interface{}{
140-
"mapX": map[string]interface{}{
141-
"arrayX": []interface{}{uint64(7), uint64(8), uint64(9)},
142-
"utf8_stringX": "hello",
143-
}})
173+
var root struct {
174+
CountryOffset uintptr `maxminddb:"country"`
144175

145-
c.Assert(result.Uint16, Equals, uint16(100))
146-
c.Assert(result.Uint32, Equals, uint32(268435456))
147-
c.Assert(result.Uint64, Equals, uint64(1152921504606846976))
148-
c.Assert(result.Utf8String, Equals, "unicode! ☯ - ♫")
149-
bigInt := new(big.Int)
150-
bigInt.SetString("1329227995784915872903807060280344576", 10)
151-
c.Assert(&result.Uint128, DeepEquals, bigInt)
176+
Location struct {
177+
Latitude float64 `maxminddb:"latitude"`
178+
// Longitude is directly nested within the parent map.
179+
LongitudeOffset uintptr `maxminddb:"longitude"`
180+
// TimeZone is indirected via a pointer.
181+
TimeZoneOffset uintptr `maxminddb:"time_zone"`
182+
} `maxminddb:"location"`
183+
}
184+
c.Check(db.Decode(off, &root), IsNil)
185+
c.Check(root.Location.Latitude, Equals, 51.5142)
152186

153-
if err = reader.Close(); err != nil {
154-
c.Assert(err, nil, "no error on close")
187+
var longitude float64
188+
c.Check(db.Decode(root.Location.LongitudeOffset, &longitude), IsNil)
189+
c.Check(longitude, Equals, -0.0931)
190+
191+
var timeZone string
192+
c.Check(db.Decode(root.Location.TimeZoneOffset, &timeZone), IsNil)
193+
c.Check(timeZone, Equals, "Europe/London")
194+
195+
var country struct {
196+
IsoCode string `maxminddb:"iso_code"`
155197
}
198+
c.Check(db.Decode(root.CountryOffset, &country), IsNil)
199+
c.Check(country.IsoCode, Equals, "GB")
200+
201+
c.Check(db.Close(), IsNil)
156202
}
157203

158204
func (s *MySuite) TestDecodingUint16IntoInt(c *C) {

verifier.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ func (v *verifier) verifySearchTree() (map[uint]bool, error) {
103103
if err != nil {
104104
return nil, err
105105
}
106-
offsets[offset] = true
106+
offsets[uint(offset)] = true
107107
}
108108
if err := it.Err(); err != nil {
109109
return nil, err

0 commit comments

Comments
 (0)