maxmind-geoip2 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +10 -5
  4. data/lib/maxmind/geoip2/client.rb +18 -9
  5. data/lib/maxmind/geoip2/model/city.rb +4 -5
  6. data/lib/maxmind/geoip2/model/connection_type.rb +3 -2
  7. data/lib/maxmind/geoip2/model/enterprise.rb +2 -3
  8. data/lib/maxmind/geoip2/model/insights.rb +3 -5
  9. data/lib/maxmind/geoip2/record/traits.rb +39 -27
  10. data/lib/maxmind/geoip2/version.rb +8 -0
  11. data/maxmind-geoip2.gemspec +7 -2
  12. data/test/data/cmd/write-test-data/main.go +68 -0
  13. data/test/data/go.mod +13 -0
  14. data/test/data/go.sum +16 -0
  15. data/test/data/perltidyrc +6 -0
  16. data/test/data/pkg/writer/decoder.go +178 -0
  17. data/test/data/pkg/writer/geoip2.go +182 -0
  18. data/test/data/pkg/writer/ip.go +39 -0
  19. data/test/data/pkg/writer/maxmind.go +245 -0
  20. data/test/data/pkg/writer/nestedstructures.go +73 -0
  21. data/test/data/pkg/writer/writer.go +58 -0
  22. data/test/data/source-data/GeoIP2-City-Test.json +322 -1
  23. data/test/data/source-data/GeoIP2-Connection-Type-Test.json +15 -10
  24. data/test/data/source-data/GeoIP2-Country-Test.json +99 -0
  25. data/test/data/source-data/GeoIP2-Domain-Test.json +5 -0
  26. data/test/data/source-data/GeoIP2-Enterprise-Test.json +347 -1
  27. data/test/data/source-data/GeoIP2-Precision-Enterprise-Sandbox-Test.json +296 -0
  28. data/test/data/source-data/GeoIP2-Precision-Enterprise-Test.json +412 -2
  29. data/test/data/source-data/GeoIP2-Static-IP-Score-Test.json +15 -0
  30. data/test/data/source-data/GeoIP2-User-Count-Test.json +18 -0
  31. data/test/data/source-data/GeoLite2-City-Test.json +168 -0
  32. data/test/data/source-data/GeoLite2-Country-Test.json +92 -0
  33. data/test/data/test-data/GeoIP2-Anonymous-IP-Test.mmdb +0 -0
  34. data/test/data/test-data/GeoIP2-City-Test-Broken-Double-Format.mmdb +0 -0
  35. data/test/data/test-data/GeoIP2-City-Test-Invalid-Node-Count.mmdb +0 -0
  36. data/test/data/test-data/GeoIP2-City-Test.mmdb +0 -0
  37. data/test/data/test-data/GeoIP2-Connection-Type-Test.mmdb +0 -0
  38. data/test/data/test-data/GeoIP2-Country-Test.mmdb +0 -0
  39. data/test/data/test-data/GeoIP2-DensityIncome-Test.mmdb +0 -0
  40. data/test/data/test-data/GeoIP2-Domain-Test.mmdb +0 -0
  41. data/test/data/test-data/GeoIP2-Enterprise-Test.mmdb +0 -0
  42. data/test/data/test-data/GeoIP2-ISP-Test.mmdb +0 -0
  43. data/test/data/test-data/GeoIP2-Precision-Enterprise-Test.mmdb +0 -0
  44. data/test/data/test-data/GeoIP2-Static-IP-Score-Test.mmdb +0 -0
  45. data/test/data/test-data/GeoIP2-User-Count-Test.mmdb +0 -0
  46. data/test/data/test-data/GeoLite2-ASN-Test.mmdb +0 -0
  47. data/test/data/test-data/GeoLite2-City-Test.mmdb +0 -0
  48. data/test/data/test-data/GeoLite2-Country-Test.mmdb +0 -0
  49. data/test/data/test-data/MaxMind-DB-no-ipv4-search-tree.mmdb +0 -0
  50. data/test/data/test-data/MaxMind-DB-string-value-entries.mmdb +0 -0
  51. data/test/data/test-data/MaxMind-DB-test-broken-pointers-24.mmdb +0 -0
  52. data/test/data/test-data/MaxMind-DB-test-broken-search-tree-24.mmdb +0 -0
  53. data/test/data/test-data/MaxMind-DB-test-decoder.mmdb +0 -0
  54. data/test/data/test-data/MaxMind-DB-test-ipv4-24.mmdb +0 -0
  55. data/test/data/test-data/MaxMind-DB-test-ipv4-28.mmdb +0 -0
  56. data/test/data/test-data/MaxMind-DB-test-ipv4-32.mmdb +0 -0
  57. data/test/data/test-data/MaxMind-DB-test-ipv6-24.mmdb +0 -0
  58. data/test/data/test-data/MaxMind-DB-test-ipv6-28.mmdb +0 -0
  59. data/test/data/test-data/MaxMind-DB-test-ipv6-32.mmdb +0 -0
  60. data/test/data/test-data/MaxMind-DB-test-metadata-pointers.mmdb +0 -0
  61. data/test/data/test-data/MaxMind-DB-test-mixed-24.mmdb +0 -0
  62. data/test/data/test-data/MaxMind-DB-test-mixed-28.mmdb +0 -0
  63. data/test/data/test-data/MaxMind-DB-test-mixed-32.mmdb +0 -0
  64. data/test/data/test-data/MaxMind-DB-test-nested.mmdb +0 -0
  65. data/test/data/test-data/MaxMind-DB-test-pointer-decoder.mmdb +0 -0
  66. data/test/data/test-data/README.md +28 -12
  67. data/test/test_client.rb +18 -2
  68. data/test/test_reader.rb +19 -1
  69. metadata +16 -7
  70. data/test/data/source-data/README +0 -15
  71. data/test/data/test-data/write-test-data.pl +0 -695
@@ -0,0 +1,178 @@
1
+ package writer
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/binary"
6
+ "fmt"
7
+ "math"
8
+ "math/big"
9
+ "net/netip"
10
+
11
+ "github.com/maxmind/mmdbwriter"
12
+ "github.com/maxmind/mmdbwriter/mmdbtype"
13
+ "go4.org/netipx"
14
+ )
15
+
16
+ // WriteDecoderTestDB writes an mmdb file with all possible record value types.
17
+ func (w *Writer) WriteDecoderTestDB() error {
18
+ dbWriter, err := mmdbwriter.New(
19
+ mmdbwriter.Options{
20
+ DatabaseType: "MaxMind DB Decoder Test",
21
+ Description: map[string]string{
22
+ "en": "MaxMind DB Decoder Test database - contains every MaxMind DB data type",
23
+ },
24
+ DisableIPv4Aliasing: false,
25
+ IncludeReservedNetworks: true,
26
+ IPVersion: 6,
27
+ Languages: []string{"en"},
28
+ RecordSize: 24,
29
+ },
30
+ )
31
+ if err != nil {
32
+ return fmt.Errorf("creating mmdbwriter: %w", err)
33
+ }
34
+
35
+ addrs, err := parseIPSlice(ipSample)
36
+ if err != nil {
37
+ return fmt.Errorf("parsing ip addresses: %w", err)
38
+ }
39
+ if err := insertAllTypes(dbWriter, addrs); err != nil {
40
+ return fmt.Errorf("inserting all types records: %w", err)
41
+ }
42
+
43
+ zeroAddr, err := netip.ParsePrefix("::0.0.0.0/128")
44
+ if err != nil {
45
+ return fmt.Errorf("parsing ip: %w", err)
46
+ }
47
+ if err := insertAllTypesZero(dbWriter, []netip.Prefix{zeroAddr}); err != nil {
48
+ return fmt.Errorf("inserting all types records: %w", err)
49
+ }
50
+
51
+ maxAddr, err := netip.ParsePrefix("::255.255.255.255/128")
52
+ if err != nil {
53
+ return fmt.Errorf("parsing ip: %w", err)
54
+ }
55
+ if err := insertNumericMax(dbWriter, []netip.Prefix{maxAddr}); err != nil {
56
+ return fmt.Errorf("inserting all types records: %w", err)
57
+ }
58
+
59
+ if err := w.write(dbWriter, "MaxMind-DB-test-decoder.mmdb"); err != nil {
60
+ return fmt.Errorf("writing database: %w", err)
61
+ }
62
+ return nil
63
+ }
64
+
65
+ // insertAllTypes inserts records with all possible value types.
66
+ func insertAllTypes(w *mmdbwriter.Tree, ipAddresses []netip.Prefix) error {
67
+ buf := new(bytes.Buffer)
68
+ if err := binary.Write(buf, binary.BigEndian, uint32(42)); err != nil {
69
+ return fmt.Errorf("creating buffer for all types record: %w", err)
70
+ }
71
+
72
+ ui64 := big.Int{}
73
+ ui64.Lsh(big.NewInt(1), 60)
74
+
75
+ ui128 := big.Int{}
76
+ ui128.Lsh(big.NewInt(1), 120)
77
+ mmdbUint128 := mmdbtype.Uint128(ui128)
78
+
79
+ allTypes := mmdbtype.Map{
80
+ "array": mmdbtype.Slice{
81
+ mmdbtype.Uint32(1),
82
+ mmdbtype.Uint32(2),
83
+ mmdbtype.Uint32(3),
84
+ },
85
+ "bytes": mmdbtype.Bytes(buf.Bytes()),
86
+ "boolean": mmdbtype.Bool(true),
87
+ "double": mmdbtype.Float64(42.123456),
88
+ "float": mmdbtype.Float32(1.1),
89
+ "int32": mmdbtype.Int32(-1 * math.Pow(2, 28)),
90
+ "map": mmdbtype.Map{
91
+ "mapX": mmdbtype.Map{
92
+ "utf8_stringX": mmdbtype.String("hello"),
93
+ "arrayX": mmdbtype.Slice{
94
+ mmdbtype.Uint32(7),
95
+ mmdbtype.Uint32(8),
96
+ mmdbtype.Uint32(9),
97
+ },
98
+ },
99
+ },
100
+ "uint16": mmdbtype.Uint16(100),
101
+ "uint32": mmdbtype.Uint32(math.Pow(2, 28)),
102
+ "uint64": mmdbtype.Uint64(ui64.Uint64()),
103
+ "uint128": mmdbUint128.Copy(),
104
+ "utf8_string": mmdbtype.String("unicode! ☯ - ♫"),
105
+ }
106
+
107
+ for _, addr := range ipAddresses {
108
+ err := w.Insert(
109
+ netipx.PrefixIPNet(addr),
110
+ allTypes,
111
+ )
112
+ if err != nil {
113
+ return fmt.Errorf("inserting ip: %w", err)
114
+ }
115
+ }
116
+ return nil
117
+ }
118
+
119
+ // insertAllTypesZero inserts records with all possible value types with zero values.
120
+ func insertAllTypesZero(w *mmdbwriter.Tree, ipAddresses []netip.Prefix) error {
121
+ var uint128 big.Int
122
+ mmdbUint128 := mmdbtype.Uint128(uint128)
123
+
124
+ zeroValues := mmdbtype.Map{
125
+ "array": mmdbtype.Slice{},
126
+ "bytes": mmdbtype.Bytes([]byte{}),
127
+ "boolean": mmdbtype.Bool(false),
128
+ "double": mmdbtype.Float64(0),
129
+ "float": mmdbtype.Float32(0),
130
+ "int32": mmdbtype.Int32(0),
131
+ "map": mmdbtype.Map{},
132
+ "uint16": mmdbtype.Uint16(0),
133
+ "uint32": mmdbtype.Uint32(0),
134
+ "uint64": mmdbtype.Uint64(0),
135
+ "uint128": mmdbUint128.Copy(),
136
+ "utf8_string": mmdbtype.String(""),
137
+ }
138
+
139
+ for _, addr := range ipAddresses {
140
+ err := w.Insert(
141
+ netipx.PrefixIPNet(addr),
142
+ zeroValues,
143
+ )
144
+ if err != nil {
145
+ return fmt.Errorf("inserting ip: %w", err)
146
+ }
147
+ }
148
+ return nil
149
+ }
150
+
151
+ // insertNumericMax inserts records with numeric types maxed out.
152
+ func insertNumericMax(w *mmdbwriter.Tree, ipAddresses []netip.Prefix) error {
153
+ var uint128Max big.Int
154
+ uint128Max.Exp(big.NewInt(2), big.NewInt(128), nil)
155
+ uint128Max.Sub(&uint128Max, big.NewInt(1))
156
+ mmdbUint128 := mmdbtype.Uint128(uint128Max)
157
+
158
+ numMax := mmdbtype.Map{
159
+ "double": mmdbtype.Float64(math.Inf(1)),
160
+ "float": mmdbtype.Float32(float32(math.Inf(1))),
161
+ "int32": mmdbtype.Int32(1<<31 - 1),
162
+ "uint16": mmdbtype.Uint16(0xffff),
163
+ "uint32": mmdbtype.Uint32(0xffffffff),
164
+ "uint64": mmdbtype.Uint64(0xffffffffffffffff),
165
+ "uint128": mmdbUint128.Copy(),
166
+ }
167
+
168
+ for _, addr := range ipAddresses {
169
+ err := w.Insert(
170
+ netipx.PrefixIPNet(addr),
171
+ numMax,
172
+ )
173
+ if err != nil {
174
+ return fmt.Errorf("inserting ip: %w", err)
175
+ }
176
+ }
177
+ return nil
178
+ }
@@ -0,0 +1,182 @@
1
+ package writer
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "net/netip"
7
+ "os"
8
+ "path/filepath"
9
+ "strings"
10
+
11
+ "github.com/maxmind/mmdbwriter"
12
+ "github.com/maxmind/mmdbwriter/mmdbtype"
13
+ "go4.org/netipx"
14
+ )
15
+
16
+ // WriteGeoIP2TestDB writes GeoIP2 test mmdb files.
17
+ func (w *Writer) WriteGeoIP2TestDB() error {
18
+ dbTypes := []string{
19
+ "GeoIP2-Anonymous-IP",
20
+ "GeoIP2-City",
21
+ "GeoIP2-Connection-Type",
22
+ "GeoIP2-Country",
23
+ "GeoIP2-DensityIncome",
24
+ "GeoIP2-Domain",
25
+ "GeoIP2-Enterprise",
26
+ "GeoIP2-ISP",
27
+ "GeoIP2-Precision-Enterprise",
28
+ "GeoIP2-Static-IP-Score",
29
+ "GeoIP2-User-Count",
30
+ "GeoLite2-ASN",
31
+ "GeoLite2-City",
32
+ "GeoLite2-Country",
33
+ }
34
+
35
+ for _, dbType := range dbTypes {
36
+ languages := []string{"en"}
37
+ description := map[string]string{
38
+ "en": strings.ReplaceAll(dbType, "-", " ") +
39
+ " Test Database (fake GeoIP2 data, for example purposes only)",
40
+ }
41
+
42
+ if dbType == "GeoIP2-City" {
43
+ languages = append(languages, "zh")
44
+ description["zh"] = "小型数据库"
45
+ }
46
+
47
+ dbWriter, err := mmdbwriter.New(
48
+ mmdbwriter.Options{
49
+ DatabaseType: dbType,
50
+ Description: description,
51
+ DisableIPv4Aliasing: false,
52
+ IPVersion: 6,
53
+ Languages: languages,
54
+ RecordSize: 28,
55
+ },
56
+ )
57
+ if err != nil {
58
+ return fmt.Errorf("creating mmdbwriter: %w", err)
59
+ }
60
+
61
+ if dbType == "GeoIP2-Anonymous-IP" {
62
+ if err := populateAllNetworks(dbWriter); err != nil {
63
+ return fmt.Errorf("inserting all networks: %w", err)
64
+ }
65
+ }
66
+
67
+ jsonFileName := fmt.Sprintf("%s-Test.json", dbType)
68
+ if err := w.insertJSON(dbWriter, jsonFileName); err != nil {
69
+ return fmt.Errorf("inserting json: %w", err)
70
+ }
71
+
72
+ dbFileName := fmt.Sprintf("%s-Test.mmdb", dbType)
73
+ if err := w.write(dbWriter, dbFileName); err != nil {
74
+ return fmt.Errorf("writing database: %w", err)
75
+ }
76
+ }
77
+
78
+ return nil
79
+ }
80
+
81
+ // insertJSON reads and parses a json file into mmdbtypes values and inserts
82
+ // them into the mmdbwriter tree.
83
+ func (w *Writer) insertJSON(dbWriter *mmdbwriter.Tree, fileName string) error {
84
+ file, err := os.Open(filepath.Clean(filepath.Join(w.source, fileName)))
85
+ if err != nil {
86
+ return fmt.Errorf("opening json file: %w", err)
87
+ }
88
+ defer file.Close()
89
+
90
+ var data []map[string]any
91
+ if err := json.NewDecoder(file).Decode(&data); err != nil {
92
+ return fmt.Errorf("decoding json file: %w", err)
93
+ }
94
+
95
+ for _, record := range data {
96
+ for k, v := range record {
97
+ prefix, err := netip.ParsePrefix(k)
98
+ if err != nil {
99
+ return fmt.Errorf("parsing ip: %w", err)
100
+ }
101
+
102
+ mmdbValue, err := toMMDBType(prefix.String(), v)
103
+ if err != nil {
104
+ return fmt.Errorf("converting value to mmdbtype: %w", err)
105
+ }
106
+
107
+ err = dbWriter.Insert(
108
+ netipx.PrefixIPNet(prefix),
109
+ mmdbValue,
110
+ )
111
+ if err != nil {
112
+ return fmt.Errorf("inserting ip: %w", err)
113
+ }
114
+ }
115
+ }
116
+ return nil
117
+ }
118
+
119
+ // toMMDBType key converts field values read from json into their corresponding mmdbtype.DataType.
120
+ // It makes some assumptions for numeric types based on previous knowledge about field types.
121
+ func toMMDBType(key string, value any) (mmdbtype.DataType, error) {
122
+ switch v := value.(type) {
123
+ case bool:
124
+ return mmdbtype.Bool(v), nil
125
+ case string:
126
+ return mmdbtype.String(v), nil
127
+ case map[string]any:
128
+ m := mmdbtype.Map{}
129
+ for innerKey, val := range v {
130
+ innerVal, err := toMMDBType(innerKey, val)
131
+ if err != nil {
132
+ return nil, fmt.Errorf("parsing mmdbtype.Map for key %q: %w", key, err)
133
+ }
134
+ m[mmdbtype.String(innerKey)] = innerVal
135
+ }
136
+ return m, nil
137
+ case []any:
138
+ s := mmdbtype.Slice{}
139
+ for _, val := range v {
140
+ innerVal, err := toMMDBType(key, val)
141
+ if err != nil {
142
+ return nil, fmt.Errorf("parsing mmdbtype.Slice for key %q: %w", key, err)
143
+ }
144
+ s = append(s, innerVal)
145
+ }
146
+ return s, nil
147
+ case float64:
148
+ switch key {
149
+ case "accuracy_radius", "confidence", "metro_code":
150
+ return mmdbtype.Uint16(v), nil
151
+ case "autonomous_system_number", "average_income",
152
+ "geoname_id", "ipv4_24", "ipv4_32", "ipv6_32",
153
+ "ipv6_48", "ipv6_64", "population_density":
154
+ return mmdbtype.Uint32(v), nil
155
+ case "ip_risk", "latitude", "longitude", "score",
156
+ "static_ip_score":
157
+ return mmdbtype.Float64(v), nil
158
+ default:
159
+ return nil, fmt.Errorf("unsupported numberic type for key %q: %T", key, value)
160
+ }
161
+ default:
162
+ return nil, fmt.Errorf("unsupported type for key %q: %T", key, value)
163
+ }
164
+ }
165
+
166
+ // populate all networks inserts all networks into the writer with an empty map value.
167
+ func populateAllNetworks(w *mmdbwriter.Tree) error {
168
+ defaultNet, err := netip.ParsePrefix("::/0")
169
+ if err != nil {
170
+ return fmt.Errorf("parsing ip: %w", err)
171
+ }
172
+
173
+ err = w.Insert(
174
+ netipx.PrefixIPNet(defaultNet),
175
+ mmdbtype.Map{},
176
+ )
177
+ if err != nil {
178
+ return fmt.Errorf("inserting ip: %w", err)
179
+ }
180
+
181
+ return nil
182
+ }
@@ -0,0 +1,39 @@
1
+ package writer
2
+
3
+ import (
4
+ "fmt"
5
+ "net/netip"
6
+
7
+ "go4.org/netipx"
8
+ )
9
+
10
+ // parseIPRange takes IP addresses in string presentation form that represent a
11
+ // range and returns an IP range.
12
+ func parseIPRange(from, to string) (netipx.IPRange, error) {
13
+ startIP, err := netip.ParseAddr(from)
14
+ if err != nil {
15
+ return netipx.IPRange{}, fmt.Errorf("parsing %s as an IP: %w", from, err)
16
+ }
17
+ endIP, err := netip.ParseAddr(to)
18
+ if err != nil {
19
+ return netipx.IPRange{}, fmt.Errorf("parsing %s as an IP: %w", to, err)
20
+ }
21
+ ipRange := netipx.IPRangeFrom(startIP, endIP)
22
+ if !ipRange.IsValid() {
23
+ return netipx.IPRange{}, fmt.Errorf("%s-%s is an invalid IP range", startIP, endIP)
24
+ }
25
+ return ipRange, nil
26
+ }
27
+
28
+ // parseIPSlice parses a slice of IP address strings and returns a slice of netip.Prefix.
29
+ func parseIPSlice(ipAddresses []string) ([]netip.Prefix, error) {
30
+ var addrs []netip.Prefix
31
+ for _, ip := range ipAddresses {
32
+ addr, err := netip.ParsePrefix(ip)
33
+ if err != nil {
34
+ return nil, fmt.Errorf("parsing %s as an IP: %w", ip, err)
35
+ }
36
+ addrs = append(addrs, addr)
37
+ }
38
+ return addrs, nil
39
+ }
@@ -0,0 +1,245 @@
1
+ package writer
2
+
3
+ import (
4
+ "fmt"
5
+ "net/netip"
6
+
7
+ "github.com/maxmind/mmdbwriter"
8
+ "github.com/maxmind/mmdbwriter/mmdbtype"
9
+ "go4.org/netipx"
10
+ )
11
+
12
+ // WriteIPv4TestDB writes mmdb files for an ip range between 1.1.1.1 and 1.1.1.32
13
+ // with various record sizes.
14
+ func (w *Writer) WriteIPv4TestDB() error {
15
+ ipRange, err := parseIPRange("1.1.1.1", "1.1.1.32")
16
+ if err != nil {
17
+ return fmt.Errorf("parsing ip range: %w", err)
18
+ }
19
+
20
+ for _, recordSize := range []int{24, 28, 32} {
21
+ err := w.writeMaxMindTestDB(
22
+ recordSize,
23
+ []netipx.IPRange{ipRange},
24
+ "ipv4",
25
+ )
26
+ if err != nil {
27
+ return fmt.Errorf("writing test database: %w", err)
28
+ }
29
+ }
30
+
31
+ return nil
32
+ }
33
+
34
+ // WriteIPv6TestDB writes mmdb files for an ip range between ::1:ffff:ffff and ::2:0000:0059
35
+ // with various record sizes.
36
+ func (w *Writer) WriteIPv6TestDB() error {
37
+ ipRange, err := parseIPRange("::1:ffff:ffff", "::2:0000:0059")
38
+ if err != nil {
39
+ return fmt.Errorf("parsing ip range: %w", err)
40
+ }
41
+
42
+ for _, recordSize := range []int{24, 28, 32} {
43
+ err := w.writeMaxMindTestDB(
44
+ recordSize,
45
+ []netipx.IPRange{ipRange},
46
+ "ipv6",
47
+ )
48
+ if err != nil {
49
+ return fmt.Errorf("writing test database: %w", err)
50
+ }
51
+ }
52
+
53
+ return nil
54
+ }
55
+
56
+ // WriteMixedIPTestDB writes mmdb files for a mixed ip version range between ::1:ffff:ffff and ::2:0000:0059
57
+ // with various record sizes.
58
+ func (w *Writer) WriteMixedIPTestDB() error {
59
+ ipv6Range, err := parseIPRange("::1:ffff:ffff", "::2:0000:0059")
60
+ if err != nil {
61
+ return fmt.Errorf("parsing ip range: %w", err)
62
+ }
63
+
64
+ ipv4Range, err := parseIPRange("1.1.1.1", "1.1.1.32")
65
+ if err != nil {
66
+ return fmt.Errorf("parsing ip range: %w", err)
67
+ }
68
+
69
+ for _, recordSize := range []int{24, 28, 32} {
70
+ err := w.writeMaxMindTestDB(
71
+ recordSize,
72
+ []netipx.IPRange{ipv6Range, ipv4Range},
73
+ "mixed",
74
+ )
75
+ if err != nil {
76
+ return fmt.Errorf("writing test database: %w", err)
77
+ }
78
+ }
79
+
80
+ return nil
81
+ }
82
+
83
+ // writeMaxMindTestDB writes test mmdb files.
84
+ func (w *Writer) writeMaxMindTestDB(
85
+ recordSize int,
86
+ ipRange []netipx.IPRange,
87
+ ipVersionName string,
88
+ ) error {
89
+ ipVersion := 6
90
+ if ipRange[0].From().Is4() {
91
+ ipVersion = 4
92
+ }
93
+
94
+ metadata := map[string]string{}
95
+ metadata["en"] = "Test Database"
96
+ metadata["zh"] = "Test Database Chinese"
97
+
98
+ dbWriter, err := mmdbwriter.New(
99
+ mmdbwriter.Options{
100
+ DatabaseType: "Test",
101
+ Description: metadata,
102
+ DisableIPv4Aliasing: ipVersion == 4,
103
+ IPVersion: ipVersion,
104
+ Languages: []string{"en", "zh"},
105
+ RecordSize: recordSize,
106
+ },
107
+ )
108
+ if err != nil {
109
+ return fmt.Errorf("creating mmdbwriter: %w", err)
110
+ }
111
+
112
+ for _, ir := range ipRange {
113
+ for _, prefix := range ir.Prefixes() {
114
+ ipString := prefix.Addr().String()
115
+ if ipVersion == 6 && prefix.Addr().Is4() {
116
+ ipString = "::" + ipString
117
+ }
118
+
119
+ err := dbWriter.Insert(
120
+ netipx.PrefixIPNet(prefix),
121
+ mmdbtype.Map{
122
+ "ip": mmdbtype.String(ipString),
123
+ },
124
+ )
125
+ if err != nil {
126
+ return fmt.Errorf("inserting ip: %w", err)
127
+ }
128
+ }
129
+ }
130
+
131
+ fileName := fmt.Sprintf("MaxMind-DB-test-%s-%d.mmdb", ipVersionName, recordSize)
132
+ if err := w.write(dbWriter, fileName); err != nil {
133
+ return fmt.Errorf("writing database: %w", err)
134
+ }
135
+
136
+ return nil
137
+ }
138
+
139
+ // WriteNoIPv4TestDB writes an mmdb file with no ipv4 records.
140
+ func (w *Writer) WriteNoIPv4TestDB() error {
141
+ dbWriter, err := mmdbwriter.New(
142
+ mmdbwriter.Options{
143
+ DatabaseType: "MaxMind DB No IPv4 Search Tree",
144
+ Description: map[string]string{
145
+ "en": "MaxMind DB No IPv4 Search Tree",
146
+ },
147
+ DisableIPv4Aliasing: true,
148
+ IncludeReservedNetworks: true,
149
+ IPVersion: 6,
150
+ Languages: []string{"en"},
151
+ RecordSize: 24,
152
+ },
153
+ )
154
+ if err != nil {
155
+ return fmt.Errorf("creating mmdbwriter: %w", err)
156
+ }
157
+
158
+ addr, err := netip.ParsePrefix("::/64")
159
+ if err != nil {
160
+ return fmt.Errorf("parsing ip: %w", err)
161
+ }
162
+
163
+ err = dbWriter.Insert(
164
+ netipx.PrefixIPNet(addr),
165
+ mmdbtype.String(addr.String()),
166
+ )
167
+ if err != nil {
168
+ return fmt.Errorf("inserting ip: %w", err)
169
+ }
170
+
171
+ if err := w.write(dbWriter, "MaxMind-DB-no-ipv4-search-tree.mmdb"); err != nil {
172
+ return fmt.Errorf("writing database: %w", err)
173
+ }
174
+ return nil
175
+ }
176
+
177
+ // WriteNoMapTestDB writes an mmdb file where each record points to
178
+ // a string value.
179
+ func (w *Writer) WriteNoMapTestDB() error {
180
+ dbWriter, err := mmdbwriter.New(
181
+ mmdbwriter.Options{
182
+ DatabaseType: "MaxMind DB String Value Entries",
183
+ Description: map[string]string{
184
+ "en": "MaxMind DB String Value Entries (no maps or arrays as values)",
185
+ },
186
+ IPVersion: 4,
187
+ Languages: []string{"en"},
188
+ RecordSize: 24,
189
+ },
190
+ )
191
+ if err != nil {
192
+ return fmt.Errorf("creating mmdbwriter: %w", err)
193
+ }
194
+
195
+ ipRange, err := parseIPRange("1.1.1.1", "1.1.1.32")
196
+ if err != nil {
197
+ return fmt.Errorf("parsing ip range: %w", err)
198
+ }
199
+
200
+ for _, prefix := range ipRange.Prefixes() {
201
+ err := dbWriter.Insert(
202
+ netipx.PrefixIPNet(prefix),
203
+ mmdbtype.String(prefix.String()),
204
+ )
205
+ if err != nil {
206
+ return fmt.Errorf("inserting ip: %w", err)
207
+ }
208
+ }
209
+
210
+ if err := w.write(dbWriter, "MaxMind-DB-string-value-entries.mmdb"); err != nil {
211
+ return fmt.Errorf("writing database: %w", err)
212
+ }
213
+ return nil
214
+ }
215
+
216
+ // WriteMetadataPointersTestDB writes an mmdb file with metadata pointers allowed.
217
+ func (w *Writer) WriteMetadataPointersTestDB() error {
218
+ repeatedString := "Lots of pointers in metadata"
219
+ dbWriter, err := mmdbwriter.New(
220
+ mmdbwriter.Options{
221
+ DatabaseType: repeatedString,
222
+ Description: map[string]string{
223
+ "en": repeatedString,
224
+ "es": repeatedString,
225
+ "zh": repeatedString,
226
+ },
227
+ DisableIPv4Aliasing: true,
228
+ IPVersion: 6,
229
+ Languages: []string{"en", "es", "zh"},
230
+ RecordSize: 24,
231
+ },
232
+ )
233
+ if err != nil {
234
+ return fmt.Errorf("creating mmdbwriter: %w", err)
235
+ }
236
+
237
+ if err := populateAllNetworks(dbWriter); err != nil {
238
+ return fmt.Errorf("inserting all networks: %w", err)
239
+ }
240
+
241
+ if err := w.write(dbWriter, "MaxMind-DB-test-metadata-pointers.mmdb"); err != nil {
242
+ return fmt.Errorf("writing database: %w", err)
243
+ }
244
+ return nil
245
+ }
@@ -0,0 +1,73 @@
1
+ package writer
2
+
3
+ import (
4
+ "fmt"
5
+ "net/netip"
6
+
7
+ "github.com/maxmind/mmdbwriter"
8
+ "github.com/maxmind/mmdbwriter/mmdbtype"
9
+ "go4.org/netipx"
10
+ )
11
+
12
+ // WriteDeeplyNestedStructuresTestDB writes an mmdb file with deeply nested record value types.
13
+ func (w *Writer) WriteDeeplyNestedStructuresTestDB() error {
14
+ dbWriter, err := mmdbwriter.New(
15
+ mmdbwriter.Options{
16
+ DatabaseType: "MaxMind DB Nested Data Structures",
17
+ Description: map[string]string{
18
+ "en": "MaxMind DB Nested Data Structures Test database - contains deeply nested map/array structures",
19
+ },
20
+ DisableIPv4Aliasing: false,
21
+ IncludeReservedNetworks: true,
22
+ IPVersion: 6,
23
+ Languages: []string{"en"},
24
+ RecordSize: 24,
25
+ },
26
+ )
27
+ if err != nil {
28
+ return fmt.Errorf("creating mmdbwriter: %w", err)
29
+ }
30
+
31
+ addrs, err := parseIPSlice(ipSample)
32
+ if err != nil {
33
+ return fmt.Errorf("parsing ip addresses: %w", err)
34
+ }
35
+ if err := insertNestedStructure(dbWriter, addrs); err != nil {
36
+ return fmt.Errorf("inserting all types records: %w", err)
37
+ }
38
+
39
+ if err := w.write(dbWriter, "MaxMind-DB-test-nested.mmdb"); err != nil {
40
+ return fmt.Errorf("writing database: %w", err)
41
+ }
42
+ return nil
43
+ }
44
+
45
+ // insertNestedStructure inserts records with deeply nested structures.
46
+ func insertNestedStructure(w *mmdbwriter.Tree, ipAddresses []netip.Prefix) error {
47
+ nestedStruct := mmdbtype.Map{
48
+ "map1": mmdbtype.Map{
49
+ "map2": mmdbtype.Map{
50
+ "array": mmdbtype.Slice{
51
+ mmdbtype.Map{
52
+ "map3": mmdbtype.Map{
53
+ "a": mmdbtype.Uint32(1),
54
+ "b": mmdbtype.Uint32(2),
55
+ "c": mmdbtype.Uint32(3),
56
+ },
57
+ },
58
+ },
59
+ },
60
+ },
61
+ }
62
+
63
+ for _, addr := range ipAddresses {
64
+ err := w.Insert(
65
+ netipx.PrefixIPNet(addr),
66
+ nestedStruct,
67
+ )
68
+ if err != nil {
69
+ return fmt.Errorf("inserting ip: %w", err)
70
+ }
71
+ }
72
+ return nil
73
+ }