Skip to content

Commit 6c9a71e

Browse files
committed
Merge pull request #23 from arbortech/offset-lookups
Add Reader.LookupOffset & Decode
2 parents 7992a44 + e2a625f commit 6c9a71e

File tree

4 files changed

+146
-66
lines changed

4 files changed

+146
-66
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: 61 additions & 33 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

@@ -99,39 +104,72 @@ func (r *Reader) startNode() (uint, error) {
99104
}
100105

101106
// Lookup takes an IP address as a net.IP structure and a pointer to the
102-
// result value to decode into. The result value pointed to must be a data
103-
// value that corresponds to a record in the database. This may include a
104-
// struct representation of the data, a map capable of holding the data or an
105-
// empty interface{} value.
107+
// result value to Decode into.
108+
func (r *Reader) Lookup(ipAddress net.IP, result interface{}) error {
109+
if pointer, err := r.lookupPointer(ipAddress); pointer == 0 {
110+
return err
111+
} else {
112+
return r.retrieveData(pointer, result)
113+
}
114+
}
115+
116+
// LookupOffset maps an argument net.IP to a corresponding record offset in the
117+
// database. NotFound is returned if no such record is found, and a record may
118+
// otherwise be extracted by passing the returned offset to Decode. LookupOffset
119+
// is an advanced API, which exists to provide clients with a means to cache
120+
// previously-decoded records.
121+
func (r *Reader) LookupOffset(ipAddress net.IP) (uintptr, error) {
122+
if pointer, err := r.lookupPointer(ipAddress); pointer == 0 {
123+
return NotFound, err
124+
} else {
125+
return r.resolveDataPointer(pointer)
126+
}
127+
}
128+
129+
// Decodes the record at |offset| into |result|. The result value pointed to
130+
// must be a data value that corresponds to a record in the database. This may
131+
// include a struct representation of the data, a map capable of holding the
132+
// data or an empty interface{} value.
106133
//
107134
// If result is a pointer to a struct, the struct need not include a field
108135
// for every value that may be in the database. If a field is not present in
109136
// the structure, the decoder will not decode that field, reducing the time
110137
// required to decode the record.
111138
//
139+
// As a special case, a struct field of type uintptr will be used to capture
140+
// the offset of the value. Decode may later be used to extract the stored
141+
// value from the offset. MaxmindDBs are highly normalized: for example in the
142+
// Cities database, all records of the same country will reference a single
143+
// representative record for that country. This uintptr behavior allows clients
144+
// to leverage this normalization in their own sub-record caching.
145+
//
112146
// Currently the decoder expect most data types to correspond exactly (e.g.,
113-
// a uint64 database type must be decoded into a uint64 Go type). In the
114-
// future, this may be made more flexible.
115-
func (r *Reader) Lookup(ipAddress net.IP, result interface{}) error {
147+
// a uint64 database type must be decoded into a uint64 Go type), with the
148+
// exception of uintptr. In the future, this may be made more flexible.
149+
func (r *Reader) Decode(offset uintptr, result interface{}) error {
150+
rv := reflect.ValueOf(result)
151+
if rv.Kind() != reflect.Ptr || rv.IsNil() {
152+
return errors.New("result param must be a pointer")
153+
}
154+
155+
_, err := r.decoder.decode(uint(offset), reflect.ValueOf(result))
156+
return err
157+
}
158+
159+
func (r *Reader) lookupPointer(ipAddress net.IP) (uint, error) {
116160
if ipAddress == nil {
117-
return errors.New("ipAddress passed to Lookup cannot be nil")
161+
return 0, errors.New("ipAddress passed to Lookup cannot be nil")
118162
}
119163

120164
ipV4Address := ipAddress.To4()
121165
if ipV4Address != nil {
122166
ipAddress = ipV4Address
123167
}
124168
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())
169+
return 0, fmt.Errorf("error looking up '%s': you attempted to look up an IPv6 address in an IPv4-only database", ipAddress.String())
126170
}
127171

128-
pointer, err := r.findAddressInTree(ipAddress)
129-
130-
if pointer == 0 {
131-
return err
132-
}
133-
134-
return r.retrieveData(pointer, result)
172+
return r.findAddressInTree(ipAddress)
135173
}
136174

137175
func (r *Reader) findAddressInTree(ipAddress net.IP) (uint, error) {
@@ -194,28 +232,18 @@ func (r *Reader) readNode(nodeNumber uint, index uint) (uint, error) {
194232
}
195233

196234
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 {
235+
if offset, err := r.resolveDataPointer(pointer); err != nil {
204236
return err
237+
} else {
238+
return r.Decode(offset, result)
205239
}
206-
207-
_, err = r.decoder.decode(offset, rv)
208-
return err
209240
}
210241

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

216-
if resolved > uint(len(r.buffer)) {
245+
if resolved > uintptr(len(r.buffer)) {
217246
return 0, newInvalidDatabaseError("the MaxMind DB file's search tree is corrupt")
218247
}
219-
220248
return resolved, nil
221249
}

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)