git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/mmdb/reader_test.go (about)

     1  package mmdb
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"math/big"
     7  	"math/rand"
     8  	"net"
     9  	"os"
    10  	"path/filepath"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/require"
    16  )
    17  
    18  func TestReader(t *testing.T) {
    19  	for _, recordSize := range []uint{24, 28, 32} {
    20  		for _, ipVersion := range []uint{4, 6} {
    21  			fileName := fmt.Sprintf(
    22  				testFile("MaxMind-DB-test-ipv%d-%d.mmdb"),
    23  				ipVersion,
    24  				recordSize,
    25  			)
    26  			reader, err := Open(fileName)
    27  			require.NoError(t, err, "unexpected error while opening database: %v", err)
    28  			checkMetadata(t, reader, ipVersion, recordSize)
    29  
    30  			if ipVersion == 4 {
    31  				checkIpv4(t, reader)
    32  			} else {
    33  				checkIpv6(t, reader)
    34  			}
    35  		}
    36  	}
    37  }
    38  
    39  func TestReaderBytes(t *testing.T) {
    40  	for _, recordSize := range []uint{24, 28, 32} {
    41  		for _, ipVersion := range []uint{4, 6} {
    42  			fileName := fmt.Sprintf(
    43  				testFile("MaxMind-DB-test-ipv%d-%d.mmdb"),
    44  				ipVersion,
    45  				recordSize,
    46  			)
    47  			bytes, err := os.ReadFile(fileName)
    48  			require.NoError(t, err)
    49  			reader, err := FromBytes(bytes)
    50  			require.NoError(t, err, "unexpected error while opening bytes: %v", err)
    51  
    52  			checkMetadata(t, reader, ipVersion, recordSize)
    53  
    54  			if ipVersion == 4 {
    55  				checkIpv4(t, reader)
    56  			} else {
    57  				checkIpv6(t, reader)
    58  			}
    59  		}
    60  	}
    61  }
    62  
    63  func TestLookupNetwork(t *testing.T) {
    64  	bigInt := new(big.Int)
    65  	bigInt.SetString("1329227995784915872903807060280344576", 10)
    66  	decoderRecord := map[string]any{
    67  		"array": []any{
    68  			uint64(1),
    69  			uint64(2),
    70  			uint64(3),
    71  		},
    72  		"boolean": true,
    73  		"bytes": []uint8{
    74  			0x0,
    75  			0x0,
    76  			0x0,
    77  			0x2a,
    78  		},
    79  		"double": 42.123456,
    80  		"float":  float32(1.1),
    81  		"int32":  -268435456,
    82  		"map": map[string]any{
    83  			"mapX": map[string]any{
    84  				"arrayX": []any{
    85  					uint64(0x7),
    86  					uint64(0x8),
    87  					uint64(0x9),
    88  				},
    89  				"utf8_stringX": "hello",
    90  			},
    91  		},
    92  		"uint128":     bigInt,
    93  		"uint16":      uint64(0x64),
    94  		"uint32":      uint64(0x10000000),
    95  		"uint64":      uint64(0x1000000000000000),
    96  		"utf8_string": "unicode! ☯ - ♫",
    97  	}
    98  
    99  	tests := []struct {
   100  		IP             net.IP
   101  		DBFile         string
   102  		ExpectedCIDR   string
   103  		ExpectedRecord any
   104  		ExpectedOK     bool
   105  	}{
   106  		{
   107  			IP:             net.ParseIP("1.1.1.1"),
   108  			DBFile:         "MaxMind-DB-test-ipv6-32.mmdb",
   109  			ExpectedCIDR:   "1.0.0.0/8",
   110  			ExpectedRecord: nil,
   111  			ExpectedOK:     false,
   112  		},
   113  		{
   114  			IP:             net.ParseIP("::1:ffff:ffff"),
   115  			DBFile:         "MaxMind-DB-test-ipv6-24.mmdb",
   116  			ExpectedCIDR:   "::1:ffff:ffff/128",
   117  			ExpectedRecord: map[string]any{"ip": "::1:ffff:ffff"},
   118  			ExpectedOK:     true,
   119  		},
   120  		{
   121  			IP:             net.ParseIP("::2:0:1"),
   122  			DBFile:         "MaxMind-DB-test-ipv6-24.mmdb",
   123  			ExpectedCIDR:   "::2:0:0/122",
   124  			ExpectedRecord: map[string]any{"ip": "::2:0:0"},
   125  			ExpectedOK:     true,
   126  		},
   127  		{
   128  			IP:             net.ParseIP("1.1.1.1"),
   129  			DBFile:         "MaxMind-DB-test-ipv4-24.mmdb",
   130  			ExpectedCIDR:   "1.1.1.1/32",
   131  			ExpectedRecord: map[string]any{"ip": "1.1.1.1"},
   132  			ExpectedOK:     true,
   133  		},
   134  		{
   135  			IP:             net.ParseIP("1.1.1.3"),
   136  			DBFile:         "MaxMind-DB-test-ipv4-24.mmdb",
   137  			ExpectedCIDR:   "1.1.1.2/31",
   138  			ExpectedRecord: map[string]any{"ip": "1.1.1.2"},
   139  			ExpectedOK:     true,
   140  		},
   141  		{
   142  			IP:             net.ParseIP("1.1.1.3"),
   143  			DBFile:         "MaxMind-DB-test-decoder.mmdb",
   144  			ExpectedCIDR:   "1.1.1.0/24",
   145  			ExpectedRecord: decoderRecord,
   146  			ExpectedOK:     true,
   147  		},
   148  		{
   149  			IP:             net.ParseIP("::ffff:1.1.1.128"),
   150  			DBFile:         "MaxMind-DB-test-decoder.mmdb",
   151  			ExpectedCIDR:   "1.1.1.0/24",
   152  			ExpectedRecord: decoderRecord,
   153  			ExpectedOK:     true,
   154  		},
   155  		{
   156  			IP:             net.ParseIP("::1.1.1.128"),
   157  			DBFile:         "MaxMind-DB-test-decoder.mmdb",
   158  			ExpectedCIDR:   "::101:100/120",
   159  			ExpectedRecord: decoderRecord,
   160  			ExpectedOK:     true,
   161  		},
   162  		{
   163  			IP:             net.ParseIP("200.0.2.1"),
   164  			DBFile:         "MaxMind-DB-no-ipv4-search-tree.mmdb",
   165  			ExpectedCIDR:   "::/64",
   166  			ExpectedRecord: "::0/64",
   167  			ExpectedOK:     true,
   168  		},
   169  		{
   170  			IP:             net.ParseIP("::200.0.2.1"),
   171  			DBFile:         "MaxMind-DB-no-ipv4-search-tree.mmdb",
   172  			ExpectedCIDR:   "::/64",
   173  			ExpectedRecord: "::0/64",
   174  			ExpectedOK:     true,
   175  		},
   176  		{
   177  			IP:             net.ParseIP("0:0:0:0:ffff:ffff:ffff:ffff"),
   178  			DBFile:         "MaxMind-DB-no-ipv4-search-tree.mmdb",
   179  			ExpectedCIDR:   "::/64",
   180  			ExpectedRecord: "::0/64",
   181  			ExpectedOK:     true,
   182  		},
   183  		{
   184  			IP:             net.ParseIP("ef00::"),
   185  			DBFile:         "MaxMind-DB-no-ipv4-search-tree.mmdb",
   186  			ExpectedCIDR:   "8000::/1",
   187  			ExpectedRecord: nil,
   188  			ExpectedOK:     false,
   189  		},
   190  	}
   191  
   192  	for _, test := range tests {
   193  		t.Run(fmt.Sprintf("%s - %s", test.DBFile, test.IP), func(t *testing.T) {
   194  			var record any
   195  			reader, err := Open(testFile(test.DBFile))
   196  			require.NoError(t, err)
   197  
   198  			network, ok, err := reader.LookupNetwork(test.IP, &record)
   199  			require.NoError(t, err)
   200  			assert.Equal(t, test.ExpectedOK, ok)
   201  			assert.Equal(t, test.ExpectedCIDR, network.String())
   202  			assert.Equal(t, test.ExpectedRecord, record)
   203  		})
   204  	}
   205  }
   206  
   207  func TestDecodingToInterface(t *testing.T) {
   208  	reader, err := Open(testFile("MaxMind-DB-test-decoder.mmdb"))
   209  	require.NoError(t, err, "unexpected error while opening database: %v", err)
   210  
   211  	var recordInterface any
   212  	err = reader.Lookup(net.ParseIP("::1.1.1.0"), &recordInterface)
   213  	require.NoError(t, err, "unexpected error while doing lookup: %v", err)
   214  
   215  	checkDecodingToInterface(t, recordInterface)
   216  }
   217  
   218  func TestMetadataPointer(t *testing.T) {
   219  	_, err := Open(testFile("MaxMind-DB-test-metadata-pointers.mmdb"))
   220  	require.NoError(t, err, "unexpected error while opening database: %v", err)
   221  }
   222  
   223  func checkDecodingToInterface(t *testing.T, recordInterface any) {
   224  	record := recordInterface.(map[string]any)
   225  	assert.Equal(t, []any{uint64(1), uint64(2), uint64(3)}, record["array"])
   226  	assert.Equal(t, true, record["boolean"])
   227  	assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x2a}, record["bytes"])
   228  	assert.Equal(t, 42.123456, record["double"])
   229  	assert.Equal(t, float32(1.1), record["float"])
   230  	assert.Equal(t, -268435456, record["int32"])
   231  	assert.Equal(t,
   232  		map[string]any{
   233  			"mapX": map[string]any{
   234  				"arrayX":       []any{uint64(7), uint64(8), uint64(9)},
   235  				"utf8_stringX": "hello",
   236  			},
   237  		},
   238  		record["map"],
   239  	)
   240  
   241  	assert.Equal(t, uint64(100), record["uint16"])
   242  	assert.Equal(t, uint64(268435456), record["uint32"])
   243  	assert.Equal(t, uint64(1152921504606846976), record["uint64"])
   244  	assert.Equal(t, "unicode! ☯ - ♫", record["utf8_string"])
   245  	bigInt := new(big.Int)
   246  	bigInt.SetString("1329227995784915872903807060280344576", 10)
   247  	assert.Equal(t, bigInt, record["uint128"])
   248  }
   249  
   250  type TestType struct {
   251  	Array      []uint         `maxminddb:"array"`
   252  	Boolean    bool           `maxminddb:"boolean"`
   253  	Bytes      []byte         `maxminddb:"bytes"`
   254  	Double     float64        `maxminddb:"double"`
   255  	Float      float32        `maxminddb:"float"`
   256  	Int32      int32          `maxminddb:"int32"`
   257  	Map        map[string]any `maxminddb:"map"`
   258  	Uint16     uint16         `maxminddb:"uint16"`
   259  	Uint32     uint32         `maxminddb:"uint32"`
   260  	Uint64     uint64         `maxminddb:"uint64"`
   261  	Uint128    big.Int        `maxminddb:"uint128"`
   262  	Utf8String string         `maxminddb:"utf8_string"`
   263  }
   264  
   265  func TestDecoder(t *testing.T) {
   266  	reader, err := Open(testFile("MaxMind-DB-test-decoder.mmdb"))
   267  	require.NoError(t, err)
   268  
   269  	verify := func(result TestType) {
   270  		assert.Equal(t, []uint{uint(1), uint(2), uint(3)}, result.Array)
   271  		assert.Equal(t, true, result.Boolean)
   272  		assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x2a}, result.Bytes)
   273  		assert.Equal(t, 42.123456, result.Double)
   274  		assert.Equal(t, float32(1.1), result.Float)
   275  		assert.Equal(t, int32(-268435456), result.Int32)
   276  
   277  		assert.Equal(t,
   278  			map[string]any{
   279  				"mapX": map[string]any{
   280  					"arrayX":       []any{uint64(7), uint64(8), uint64(9)},
   281  					"utf8_stringX": "hello",
   282  				},
   283  			},
   284  			result.Map,
   285  		)
   286  
   287  		assert.Equal(t, uint16(100), result.Uint16)
   288  		assert.Equal(t, uint32(268435456), result.Uint32)
   289  		assert.Equal(t, uint64(1152921504606846976), result.Uint64)
   290  		assert.Equal(t, "unicode! ☯ - ♫", result.Utf8String)
   291  		bigInt := new(big.Int)
   292  		bigInt.SetString("1329227995784915872903807060280344576", 10)
   293  		assert.Equal(t, bigInt, &result.Uint128)
   294  	}
   295  
   296  	{
   297  		// Directly lookup and decode.
   298  		var result TestType
   299  		require.NoError(t, reader.Lookup(net.ParseIP("::1.1.1.0"), &result))
   300  		verify(result)
   301  	}
   302  	{
   303  		// Lookup record offset, then Decode.
   304  		var result TestType
   305  		offset, err := reader.LookupOffset(net.ParseIP("::1.1.1.0"))
   306  		require.NoError(t, err)
   307  		assert.NotEqual(t, NotFound, offset)
   308  
   309  		assert.NoError(t, reader.Decode(offset, &result))
   310  		verify(result)
   311  	}
   312  
   313  	assert.NoError(t, reader.Close())
   314  }
   315  
   316  type TestInterface interface {
   317  	method() bool
   318  }
   319  
   320  func (t *TestType) method() bool {
   321  	return t.Boolean
   322  }
   323  
   324  func TestStructInterface(t *testing.T) {
   325  	var result TestInterface = &TestType{}
   326  
   327  	reader, err := Open(testFile("MaxMind-DB-test-decoder.mmdb"))
   328  	require.NoError(t, err)
   329  
   330  	require.NoError(t, reader.Lookup(net.ParseIP("::1.1.1.0"), &result))
   331  
   332  	assert.Equal(t, true, result.method())
   333  }
   334  
   335  func TestNonEmptyNilInterface(t *testing.T) {
   336  	var result TestInterface
   337  
   338  	reader, err := Open(testFile("MaxMind-DB-test-decoder.mmdb"))
   339  	require.NoError(t, err)
   340  
   341  	err = reader.Lookup(net.ParseIP("::1.1.1.0"), &result)
   342  	assert.Equal(
   343  		t,
   344  		"mmdb: cannot unmarshal map into type mmdb.TestInterface",
   345  		err.Error(),
   346  	)
   347  }
   348  
   349  type CityTraits struct {
   350  	AutonomousSystemNumber uint `json:"autonomous_system_number,omitempty" mmdb:"autonomous_system_number"`
   351  }
   352  
   353  type City struct {
   354  	Traits CityTraits `maxminddb:"traits"`
   355  }
   356  
   357  func TestEmbeddedStructAsInterface(t *testing.T) {
   358  	var city City
   359  	var result any = city.Traits
   360  
   361  	db, err := Open(testFile("GeoIP2-ISP-Test.mmdb"))
   362  	require.NoError(t, err)
   363  
   364  	assert.NoError(t, db.Lookup(net.ParseIP("1.128.0.0"), &result))
   365  }
   366  
   367  type BoolInterface interface {
   368  	true() bool
   369  }
   370  
   371  type Bool bool
   372  
   373  func (b Bool) true() bool {
   374  	return bool(b)
   375  }
   376  
   377  type ValueTypeTestType struct {
   378  	Boolean BoolInterface `maxminddb:"boolean"`
   379  }
   380  
   381  func TestValueTypeInterface(t *testing.T) {
   382  	var result ValueTypeTestType
   383  	result.Boolean = Bool(false)
   384  
   385  	reader, err := Open(testFile("MaxMind-DB-test-decoder.mmdb"))
   386  	require.NoError(t, err)
   387  
   388  	// although it would be nice to support cases like this, I am not sure it
   389  	// is possible to do so in a general way.
   390  	assert.Error(t, reader.Lookup(net.ParseIP("::1.1.1.0"), &result))
   391  }
   392  
   393  type NestedMapX struct {
   394  	UTF8StringX string `maxminddb:"utf8_stringX"`
   395  }
   396  
   397  type NestedPointerMapX struct {
   398  	ArrayX []int `maxminddb:"arrayX"`
   399  }
   400  
   401  type PointerMap struct {
   402  	MapX struct {
   403  		Ignored string
   404  		NestedMapX
   405  		*NestedPointerMapX
   406  	} `maxminddb:"mapX"`
   407  }
   408  
   409  type TestPointerType struct {
   410  	Array   *[]uint     `maxminddb:"array"`
   411  	Boolean *bool       `maxminddb:"boolean"`
   412  	Bytes   *[]byte     `maxminddb:"bytes"`
   413  	Double  *float64    `maxminddb:"double"`
   414  	Float   *float32    `maxminddb:"float"`
   415  	Int32   *int32      `maxminddb:"int32"`
   416  	Map     *PointerMap `maxminddb:"map"`
   417  	Uint16  *uint16     `maxminddb:"uint16"`
   418  	Uint32  *uint32     `maxminddb:"uint32"`
   419  
   420  	// Test for pointer to pointer
   421  	Uint64     **uint64 `maxminddb:"uint64"`
   422  	Uint128    *big.Int `maxminddb:"uint128"`
   423  	Utf8String *string  `maxminddb:"utf8_string"`
   424  }
   425  
   426  func TestComplexStructWithNestingAndPointer(t *testing.T) {
   427  	reader, err := Open(testFile("MaxMind-DB-test-decoder.mmdb"))
   428  	assert.NoError(t, err)
   429  
   430  	var result TestPointerType
   431  
   432  	err = reader.Lookup(net.ParseIP("::1.1.1.0"), &result)
   433  	require.NoError(t, err)
   434  
   435  	assert.Equal(t, []uint{uint(1), uint(2), uint(3)}, *result.Array)
   436  	assert.Equal(t, true, *result.Boolean)
   437  	assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x2a}, *result.Bytes)
   438  	assert.Equal(t, 42.123456, *result.Double)
   439  	assert.Equal(t, float32(1.1), *result.Float)
   440  	assert.Equal(t, int32(-268435456), *result.Int32)
   441  
   442  	assert.Equal(t, []int{7, 8, 9}, result.Map.MapX.ArrayX)
   443  
   444  	assert.Equal(t, "hello", result.Map.MapX.UTF8StringX)
   445  
   446  	assert.Equal(t, uint16(100), *result.Uint16)
   447  	assert.Equal(t, uint32(268435456), *result.Uint32)
   448  	assert.Equal(t, uint64(1152921504606846976), **result.Uint64)
   449  	assert.Equal(t, "unicode! ☯ - ♫", *result.Utf8String)
   450  	bigInt := new(big.Int)
   451  	bigInt.SetString("1329227995784915872903807060280344576", 10)
   452  	assert.Equal(t, bigInt, result.Uint128)
   453  
   454  	assert.NoError(t, reader.Close())
   455  }
   456  
   457  func TestNestedOffsetDecode(t *testing.T) {
   458  	db, err := Open(testFile("GeoIP2-City-Test.mmdb"))
   459  	require.NoError(t, err)
   460  
   461  	off, err := db.LookupOffset(net.ParseIP("81.2.69.142"))
   462  	assert.NotEqual(t, off, NotFound)
   463  	require.NoError(t, err)
   464  
   465  	var root struct {
   466  		CountryOffset uintptr `maxminddb:"country"`
   467  
   468  		Location struct {
   469  			Latitude float64 `maxminddb:"latitude"`
   470  			// Longitude is directly nested within the parent map.
   471  			LongitudeOffset uintptr `maxminddb:"longitude"`
   472  			// TimeZone is indirected via a pointer.
   473  			TimeZoneOffset uintptr `maxminddb:"time_zone"`
   474  		} `maxminddb:"location"`
   475  	}
   476  	assert.NoError(t, db.Decode(off, &root))
   477  	assert.Equal(t, 51.5142, root.Location.Latitude)
   478  
   479  	var longitude float64
   480  	assert.NoError(t, db.Decode(root.Location.LongitudeOffset, &longitude))
   481  	assert.Equal(t, -0.0931, longitude)
   482  
   483  	var timeZone string
   484  	assert.NoError(t, db.Decode(root.Location.TimeZoneOffset, &timeZone))
   485  	assert.Equal(t, "Europe/London", timeZone)
   486  
   487  	var country struct {
   488  		IsoCode string `maxminddb:"iso_code"`
   489  	}
   490  	assert.NoError(t, db.Decode(root.CountryOffset, &country))
   491  	assert.Equal(t, "GB", country.IsoCode)
   492  
   493  	assert.NoError(t, db.Close())
   494  }
   495  
   496  func TestDecodingUint16IntoInt(t *testing.T) {
   497  	reader, err := Open(testFile("MaxMind-DB-test-decoder.mmdb"))
   498  	require.NoError(t, err, "unexpected error while opening database: %v", err)
   499  
   500  	var result struct {
   501  		Uint16 int `maxminddb:"uint16"`
   502  	}
   503  	err = reader.Lookup(net.ParseIP("::1.1.1.0"), &result)
   504  	require.NoError(t, err)
   505  
   506  	assert.Equal(t, 100, result.Uint16)
   507  }
   508  
   509  func TestIpv6inIpv4(t *testing.T) {
   510  	reader, err := Open(testFile("MaxMind-DB-test-ipv4-24.mmdb"))
   511  	require.NoError(t, err, "unexpected error while opening database: %v", err)
   512  
   513  	var result TestType
   514  	err = reader.Lookup(net.ParseIP("2001::"), &result)
   515  
   516  	var emptyResult TestType
   517  	assert.Equal(t, emptyResult, result)
   518  
   519  	expected := errors.New(
   520  		"error looking up '2001::': you attempted to look up an IPv6 address in an IPv4-only database",
   521  	)
   522  	assert.Equal(t, expected, err)
   523  	assert.NoError(t, reader.Close(), "error on close")
   524  }
   525  
   526  func TestBrokenDoubleDatabase(t *testing.T) {
   527  	reader, err := Open(testFile("GeoIP2-City-Test-Broken-Double-Format.mmdb"))
   528  	require.NoError(t, err, "unexpected error while opening database: %v", err)
   529  
   530  	var result any
   531  	err = reader.Lookup(net.ParseIP("2001:220::"), &result)
   532  
   533  	expected := newInvalidDatabaseError(
   534  		"the MaxMind DB file's data section contains bad data (float 64 size of 2)",
   535  	)
   536  	assert.Equal(t, expected, err)
   537  	assert.NoError(t, reader.Close(), "error on close")
   538  }
   539  
   540  func TestInvalidNodeCountDatabase(t *testing.T) {
   541  	_, err := Open(testFile("GeoIP2-City-Test-Invalid-Node-Count.mmdb"))
   542  
   543  	expected := newInvalidDatabaseError("the MaxMind DB contains invalid metadata")
   544  	assert.Equal(t, expected, err)
   545  }
   546  
   547  func TestMissingDatabase(t *testing.T) {
   548  	reader, err := Open("file-does-not-exist.mmdb")
   549  	assert.Nil(t, reader, "received reader when doing lookups on DB that doesn't exist")
   550  	assert.Regexp(t, "open file-does-not-exist.mmdb.*", err)
   551  }
   552  
   553  func TestNonDatabase(t *testing.T) {
   554  	reader, err := Open("README.md")
   555  	assert.Nil(t, reader, "received reader when doing lookups on DB that doesn't exist")
   556  	assert.Equal(t, "error opening database: invalid MaxMind DB file", err.Error())
   557  }
   558  
   559  func TestDecodingToNonPointer(t *testing.T) {
   560  	reader, err := Open(testFile("MaxMind-DB-test-decoder.mmdb"))
   561  	require.NoError(t, err)
   562  
   563  	var recordInterface any
   564  	err = reader.Lookup(net.ParseIP("::1.1.1.0"), recordInterface)
   565  	assert.Equal(t, "result param must be a pointer", err.Error())
   566  	assert.NoError(t, reader.Close(), "error on close")
   567  }
   568  
   569  func TestNilLookup(t *testing.T) {
   570  	reader, err := Open(testFile("MaxMind-DB-test-decoder.mmdb"))
   571  	require.NoError(t, err)
   572  
   573  	var recordInterface any
   574  	err = reader.Lookup(nil, recordInterface)
   575  	assert.Equal(t, "IP passed to Lookup cannot be nil", err.Error())
   576  	assert.NoError(t, reader.Close(), "error on close")
   577  }
   578  
   579  func TestUsingClosedDatabase(t *testing.T) {
   580  	reader, err := Open(testFile("MaxMind-DB-test-decoder.mmdb"))
   581  	require.NoError(t, err)
   582  	require.NoError(t, reader.Close())
   583  
   584  	var recordInterface any
   585  
   586  	err = reader.Lookup(nil, recordInterface)
   587  	assert.Equal(t, "cannot call Lookup on a closed database", err.Error())
   588  
   589  	_, err = reader.LookupOffset(nil)
   590  	assert.Equal(t, "cannot call LookupOffset on a closed database", err.Error())
   591  
   592  	err = reader.Decode(0, recordInterface)
   593  	assert.Equal(t, "cannot call Decode on a closed database", err.Error())
   594  }
   595  
   596  func checkMetadata(t *testing.T, reader *Reader, ipVersion, recordSize uint) {
   597  	metadata := reader.Metadata
   598  
   599  	assert.Equal(t, uint(2), metadata.BinaryFormatMajorVersion)
   600  
   601  	assert.Equal(t, uint(0), metadata.BinaryFormatMinorVersion)
   602  	assert.IsType(t, uint(0), metadata.BuildEpoch)
   603  	assert.Equal(t, "Test", metadata.DatabaseType)
   604  
   605  	assert.Equal(t, metadata.Description,
   606  		map[string]string{
   607  			"en": "Test Database",
   608  			"zh": "Test Database Chinese",
   609  		})
   610  	assert.Equal(t, ipVersion, metadata.IPVersion)
   611  	assert.Equal(t, []string{"en", "zh"}, metadata.Languages)
   612  
   613  	if ipVersion == 4 {
   614  		assert.Equal(t, uint(164), metadata.NodeCount)
   615  	} else {
   616  		assert.Equal(t, uint(416), metadata.NodeCount)
   617  	}
   618  
   619  	assert.Equal(t, recordSize, metadata.RecordSize)
   620  }
   621  
   622  func checkIpv4(t *testing.T, reader *Reader) {
   623  	for i := uint(0); i < 6; i++ {
   624  		address := fmt.Sprintf("1.1.1.%d", uint(1)<<i)
   625  		ip := net.ParseIP(address)
   626  
   627  		var result map[string]string
   628  		err := reader.Lookup(ip, &result)
   629  		assert.NoError(t, err, "unexpected error while doing lookup: %v", err)
   630  		assert.Equal(t, map[string]string{"ip": address}, result)
   631  	}
   632  	pairs := map[string]string{
   633  		"1.1.1.3":  "1.1.1.2",
   634  		"1.1.1.5":  "1.1.1.4",
   635  		"1.1.1.7":  "1.1.1.4",
   636  		"1.1.1.9":  "1.1.1.8",
   637  		"1.1.1.15": "1.1.1.8",
   638  		"1.1.1.17": "1.1.1.16",
   639  		"1.1.1.31": "1.1.1.16",
   640  	}
   641  
   642  	for keyAddress, valueAddress := range pairs {
   643  		data := map[string]string{"ip": valueAddress}
   644  
   645  		ip := net.ParseIP(keyAddress)
   646  
   647  		var result map[string]string
   648  		err := reader.Lookup(ip, &result)
   649  		assert.NoError(t, err, "unexpected error while doing lookup: %v", err)
   650  		assert.Equal(t, data, result)
   651  	}
   652  
   653  	for _, address := range []string{"1.1.1.33", "255.254.253.123"} {
   654  		ip := net.ParseIP(address)
   655  
   656  		var result map[string]string
   657  		err := reader.Lookup(ip, &result)
   658  		assert.NoError(t, err, "unexpected error while doing lookup: %v", err)
   659  		assert.Nil(t, result)
   660  	}
   661  }
   662  
   663  func checkIpv6(t *testing.T, reader *Reader) {
   664  	subnets := []string{
   665  		"::1:ffff:ffff", "::2:0:0",
   666  		"::2:0:40", "::2:0:50", "::2:0:58",
   667  	}
   668  
   669  	for _, address := range subnets {
   670  		var result map[string]string
   671  		err := reader.Lookup(net.ParseIP(address), &result)
   672  		assert.NoError(t, err, "unexpected error while doing lookup: %v", err)
   673  		assert.Equal(t, map[string]string{"ip": address}, result)
   674  	}
   675  
   676  	pairs := map[string]string{
   677  		"::2:0:1":  "::2:0:0",
   678  		"::2:0:33": "::2:0:0",
   679  		"::2:0:39": "::2:0:0",
   680  		"::2:0:41": "::2:0:40",
   681  		"::2:0:49": "::2:0:40",
   682  		"::2:0:52": "::2:0:50",
   683  		"::2:0:57": "::2:0:50",
   684  		"::2:0:59": "::2:0:58",
   685  	}
   686  
   687  	for keyAddress, valueAddress := range pairs {
   688  		data := map[string]string{"ip": valueAddress}
   689  		var result map[string]string
   690  		err := reader.Lookup(net.ParseIP(keyAddress), &result)
   691  		assert.NoError(t, err, "unexpected error while doing lookup: %v", err)
   692  		assert.Equal(t, data, result)
   693  	}
   694  
   695  	for _, address := range []string{"1.1.1.33", "255.254.253.123", "89fa::"} {
   696  		var result map[string]string
   697  		err := reader.Lookup(net.ParseIP(address), &result)
   698  		assert.NoError(t, err, "unexpected error while doing lookup: %v", err)
   699  		assert.Nil(t, result)
   700  	}
   701  }
   702  
   703  func BenchmarkOpen(b *testing.B) {
   704  	var db *Reader
   705  	var err error
   706  	for i := 0; i < b.N; i++ {
   707  		db, err = Open("GeoLite2-City.mmdb")
   708  		if err != nil {
   709  			b.Error(err)
   710  		}
   711  	}
   712  	assert.NotNil(b, db)
   713  	assert.NoError(b, db.Close(), "error on close")
   714  }
   715  
   716  func BenchmarkInterfaceLookup(b *testing.B) {
   717  	db, err := Open("GeoLite2-City.mmdb")
   718  	require.NoError(b, err)
   719  
   720  	//nolint:gosec // this is a test
   721  	r := rand.New(rand.NewSource(time.Now().UnixNano()))
   722  	var result any
   723  
   724  	ip := make(net.IP, 4)
   725  	for i := 0; i < b.N; i++ {
   726  		randomIPv4Address(r, ip)
   727  		err = db.Lookup(ip, &result)
   728  		if err != nil {
   729  			b.Error(err)
   730  		}
   731  	}
   732  	assert.NoError(b, db.Close(), "error on close")
   733  }
   734  
   735  func BenchmarkInterfaceLookupNetwork(b *testing.B) {
   736  	db, err := Open("GeoLite2-City.mmdb")
   737  	require.NoError(b, err)
   738  
   739  	//nolint:gosec // this is a test
   740  	r := rand.New(rand.NewSource(time.Now().UnixNano()))
   741  	var result any
   742  
   743  	ip := make(net.IP, 4)
   744  	for i := 0; i < b.N; i++ {
   745  		randomIPv4Address(r, ip)
   746  		_, _, err = db.LookupNetwork(ip, &result)
   747  		if err != nil {
   748  			b.Error(err)
   749  		}
   750  	}
   751  	assert.NoError(b, db.Close(), "error on close")
   752  }
   753  
   754  type fullCity struct {
   755  	City struct {
   756  		GeoNameID uint              `maxminddb:"geoname_id"`
   757  		Names     map[string]string `maxminddb:"names"`
   758  	} `maxminddb:"city"`
   759  	Continent struct {
   760  		Code      string            `maxminddb:"code"`
   761  		GeoNameID uint              `maxminddb:"geoname_id"`
   762  		Names     map[string]string `maxminddb:"names"`
   763  	} `maxminddb:"continent"`
   764  	Country struct {
   765  		GeoNameID         uint              `maxminddb:"geoname_id"`
   766  		IsInEuropeanUnion bool              `maxminddb:"is_in_european_union"`
   767  		IsoCode           string            `maxminddb:"iso_code"`
   768  		Names             map[string]string `maxminddb:"names"`
   769  	} `maxminddb:"country"`
   770  	Location struct {
   771  		AccuracyRadius uint16  `maxminddb:"accuracy_radius"`
   772  		Latitude       float64 `maxminddb:"latitude"`
   773  		Longitude      float64 `maxminddb:"longitude"`
   774  		MetroCode      uint    `maxminddb:"metro_code"`
   775  		TimeZone       string  `maxminddb:"time_zone"`
   776  	} `maxminddb:"location"`
   777  	Postal struct {
   778  		Code string `maxminddb:"code"`
   779  	} `maxminddb:"postal"`
   780  	RegisteredCountry struct {
   781  		GeoNameID         uint              `maxminddb:"geoname_id"`
   782  		IsInEuropeanUnion bool              `maxminddb:"is_in_european_union"`
   783  		IsoCode           string            `maxminddb:"iso_code"`
   784  		Names             map[string]string `maxminddb:"names"`
   785  	} `maxminddb:"registered_country"`
   786  	RepresentedCountry struct {
   787  		GeoNameID         uint              `maxminddb:"geoname_id"`
   788  		IsInEuropeanUnion bool              `maxminddb:"is_in_european_union"`
   789  		IsoCode           string            `maxminddb:"iso_code"`
   790  		Names             map[string]string `maxminddb:"names"`
   791  		Type              string            `maxminddb:"type"`
   792  	} `maxminddb:"represented_country"`
   793  	Subdivisions []struct {
   794  		GeoNameID uint              `maxminddb:"geoname_id"`
   795  		IsoCode   string            `maxminddb:"iso_code"`
   796  		Names     map[string]string `maxminddb:"names"`
   797  	} `maxminddb:"subdivisions"`
   798  	Traits struct {
   799  		IsAnonymousProxy    bool `maxminddb:"is_anonymous_proxy"`
   800  		IsSatelliteProvider bool `maxminddb:"is_satellite_provider"`
   801  	} `maxminddb:"traits"`
   802  }
   803  
   804  func BenchmarkCityLookup(b *testing.B) {
   805  	db, err := Open("GeoLite2-City.mmdb")
   806  	require.NoError(b, err)
   807  
   808  	//nolint:gosec // this is a test
   809  	r := rand.New(rand.NewSource(time.Now().UnixNano()))
   810  	var result fullCity
   811  
   812  	ip := make(net.IP, 4)
   813  	for i := 0; i < b.N; i++ {
   814  		randomIPv4Address(r, ip)
   815  		err = db.Lookup(ip, &result)
   816  		if err != nil {
   817  			b.Error(err)
   818  		}
   819  	}
   820  	assert.NoError(b, db.Close(), "error on close")
   821  }
   822  
   823  func BenchmarkCityLookupNetwork(b *testing.B) {
   824  	db, err := Open("GeoLite2-City.mmdb")
   825  	require.NoError(b, err)
   826  
   827  	//nolint:gosec // this is a test
   828  	r := rand.New(rand.NewSource(time.Now().UnixNano()))
   829  	var result fullCity
   830  
   831  	ip := make(net.IP, 4)
   832  	for i := 0; i < b.N; i++ {
   833  		randomIPv4Address(r, ip)
   834  		_, _, err = db.LookupNetwork(ip, &result)
   835  		if err != nil {
   836  			b.Error(err)
   837  		}
   838  	}
   839  	assert.NoError(b, db.Close(), "error on close")
   840  }
   841  
   842  func BenchmarkCountryCode(b *testing.B) {
   843  	db, err := Open("GeoLite2-City.mmdb")
   844  	require.NoError(b, err)
   845  
   846  	type MinCountry struct {
   847  		Country struct {
   848  			IsoCode string `maxminddb:"iso_code"`
   849  		} `maxminddb:"country"`
   850  	}
   851  
   852  	//nolint:gosec // this is a test
   853  	r := rand.New(rand.NewSource(0))
   854  	var result MinCountry
   855  
   856  	ip := make(net.IP, 4)
   857  	for i := 0; i < b.N; i++ {
   858  		randomIPv4Address(r, ip)
   859  		err = db.Lookup(ip, &result)
   860  		if err != nil {
   861  			b.Error(err)
   862  		}
   863  	}
   864  	assert.NoError(b, db.Close(), "error on close")
   865  }
   866  
   867  func randomIPv4Address(r *rand.Rand, ip []byte) {
   868  	num := r.Uint32()
   869  	ip[0] = byte(num >> 24)
   870  	ip[1] = byte(num >> 16)
   871  	ip[2] = byte(num >> 8)
   872  	ip[3] = byte(num)
   873  }
   874  
   875  func testFile(file string) string {
   876  	return filepath.Join("test-data", "test-data", file)
   877  }