maxmind-db 1.0.0.beta

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 (78) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +4 -0
  3. data/LICENSE-APACHE +202 -0
  4. data/LICENSE-MIT +17 -0
  5. data/README.dev.md +30 -0
  6. data/README.md +54 -0
  7. data/Rakefile +12 -0
  8. data/bin/mmdb-benchmark.rb +61 -0
  9. data/lib/maxmind/db.rb +274 -0
  10. data/lib/maxmind/db/decoder.rb +232 -0
  11. data/lib/maxmind/db/errors.rb +8 -0
  12. data/lib/maxmind/db/file_reader.rb +37 -0
  13. data/lib/maxmind/db/memory_reader.rb +24 -0
  14. data/lib/maxmind/db/metadata.rb +61 -0
  15. data/maxmind-db.gemspec +19 -0
  16. data/test/data/LICENSE +4 -0
  17. data/test/data/MaxMind-DB-spec.md +558 -0
  18. data/test/data/MaxMind-DB-test-metadata-pointers.mmdb +0 -0
  19. data/test/data/README.md +4 -0
  20. data/test/data/bad-data/README.md +7 -0
  21. data/test/data/bad-data/libmaxminddb/libmaxminddb-offset-integer-overflow.mmdb +0 -0
  22. data/test/data/bad-data/maxminddb-golang/cyclic-data-structure.mmdb +0 -0
  23. data/test/data/bad-data/maxminddb-golang/invalid-bytes-length.mmdb +1 -0
  24. data/test/data/bad-data/maxminddb-golang/invalid-data-record-offset.mmdb +0 -0
  25. data/test/data/bad-data/maxminddb-golang/invalid-map-key-length.mmdb +0 -0
  26. data/test/data/bad-data/maxminddb-golang/invalid-string-length.mmdb +1 -0
  27. data/test/data/bad-data/maxminddb-golang/metadata-is-an-uint128.mmdb +1 -0
  28. data/test/data/bad-data/maxminddb-golang/unexpected-bytes.mmdb +0 -0
  29. data/test/data/perltidyrc +12 -0
  30. data/test/data/source-data/GeoIP2-Anonymous-IP-Test.json +41 -0
  31. data/test/data/source-data/GeoIP2-City-Test.json +12852 -0
  32. data/test/data/source-data/GeoIP2-Connection-Type-Test.json +102 -0
  33. data/test/data/source-data/GeoIP2-Country-Test.json +11347 -0
  34. data/test/data/source-data/GeoIP2-DensityIncome-Test.json +14 -0
  35. data/test/data/source-data/GeoIP2-Domain-Test.json +452 -0
  36. data/test/data/source-data/GeoIP2-Enterprise-Test.json +673 -0
  37. data/test/data/source-data/GeoIP2-ISP-Test.json +12585 -0
  38. data/test/data/source-data/GeoIP2-Precision-Enterprise-Test.json +1598 -0
  39. data/test/data/source-data/GeoIP2-User-Count-Test.json +2824 -0
  40. data/test/data/source-data/GeoLite2-ASN-Test.json +37 -0
  41. data/test/data/source-data/README +15 -0
  42. data/test/data/test-data/GeoIP2-Anonymous-IP-Test.mmdb +0 -0
  43. data/test/data/test-data/GeoIP2-City-Test-Broken-Double-Format.mmdb +0 -0
  44. data/test/data/test-data/GeoIP2-City-Test-Invalid-Node-Count.mmdb +0 -0
  45. data/test/data/test-data/GeoIP2-City-Test.mmdb +0 -0
  46. data/test/data/test-data/GeoIP2-Connection-Type-Test.mmdb +0 -0
  47. data/test/data/test-data/GeoIP2-Country-Test.mmdb +0 -0
  48. data/test/data/test-data/GeoIP2-DensityIncome-Test.mmdb +0 -0
  49. data/test/data/test-data/GeoIP2-Domain-Test.mmdb +0 -0
  50. data/test/data/test-data/GeoIP2-Enterprise-Test.mmdb +0 -0
  51. data/test/data/test-data/GeoIP2-ISP-Test.mmdb +0 -0
  52. data/test/data/test-data/GeoIP2-Precision-Enterprise-Test.mmdb +0 -0
  53. data/test/data/test-data/GeoIP2-User-Count-Test.mmdb +0 -0
  54. data/test/data/test-data/GeoLite2-ASN-Test.mmdb +0 -0
  55. data/test/data/test-data/MaxMind-DB-no-ipv4-search-tree.mmdb +0 -0
  56. data/test/data/test-data/MaxMind-DB-string-value-entries.mmdb +0 -0
  57. data/test/data/test-data/MaxMind-DB-test-broken-pointers-24.mmdb +0 -0
  58. data/test/data/test-data/MaxMind-DB-test-broken-search-tree-24.mmdb +0 -0
  59. data/test/data/test-data/MaxMind-DB-test-decoder.mmdb +0 -0
  60. data/test/data/test-data/MaxMind-DB-test-ipv4-24.mmdb +0 -0
  61. data/test/data/test-data/MaxMind-DB-test-ipv4-28.mmdb +0 -0
  62. data/test/data/test-data/MaxMind-DB-test-ipv4-32.mmdb +0 -0
  63. data/test/data/test-data/MaxMind-DB-test-ipv6-24.mmdb +0 -0
  64. data/test/data/test-data/MaxMind-DB-test-ipv6-28.mmdb +0 -0
  65. data/test/data/test-data/MaxMind-DB-test-ipv6-32.mmdb +0 -0
  66. data/test/data/test-data/MaxMind-DB-test-metadata-pointers.mmdb +0 -0
  67. data/test/data/test-data/MaxMind-DB-test-mixed-24.mmdb +0 -0
  68. data/test/data/test-data/MaxMind-DB-test-mixed-28.mmdb +0 -0
  69. data/test/data/test-data/MaxMind-DB-test-mixed-32.mmdb +0 -0
  70. data/test/data/test-data/MaxMind-DB-test-nested.mmdb +0 -0
  71. data/test/data/test-data/README.md +26 -0
  72. data/test/data/test-data/maps-with-pointers.raw +0 -0
  73. data/test/data/test-data/write-test-data.pl +620 -0
  74. data/test/data/tidyall.ini +5 -0
  75. data/test/mmdb_util.rb +24 -0
  76. data/test/test_decoder.rb +241 -0
  77. data/test/test_reader.rb +415 -0
  78. metadata +126 -0
@@ -0,0 +1,5 @@
1
+ [PerlTidy]
2
+ select = **/*.{pl,pm,t}
3
+
4
+ [JSON]
5
+ select = **/*.json
@@ -0,0 +1,24 @@
1
+ class MMDBUtil # :nodoc:
2
+ def self.make_metadata_map(record_size)
3
+ # Map
4
+ "\xe9".b +
5
+ # node_count => 0
6
+ "\x4anode_count\xc0".b +
7
+ # record_size => 28 would be \xa1\x1c
8
+ "\x4brecord_size\xa1".b + record_size.chr.b +
9
+ # ip_version => 4
10
+ "\x4aip_version\xa1\x04".b +
11
+ # database_type => 'test'
12
+ "\x4ddatabase_type\x44test".b +
13
+ # languages => ['en']
14
+ "\x49languages\x01\x04\x42en".b +
15
+ # binary_format_major_version => 2
16
+ "\x5bbinary_format_major_version\xa1\x02".b +
17
+ # binary_format_minor_version => 0
18
+ "\x5bbinary_format_minor_version\xa0".b +
19
+ # build_epoch => 0
20
+ "\x4bbuild_epoch\x00\x02".b +
21
+ # description => 'hi'
22
+ "\x4bdescription\x42hi".b
23
+ end
24
+ end
@@ -0,0 +1,241 @@
1
+ require 'maxmind/db'
2
+ require 'minitest/autorun'
3
+ require 'mmdb_util'
4
+
5
+ class DecoderTest < Minitest::Test # :nodoc:
6
+ def test_arrays
7
+ arrays = {
8
+ "\x00\x04".b => [],
9
+ "\x01\x04\x43\x46\x6f\x6f".b => ['Foo'],
10
+ "\x02\x04\x43\x46\x6f\x6f\x43\xe4\xba\xba".b => %w[Foo 人],
11
+ }
12
+ validate_type_decoding('arrays', arrays)
13
+ end
14
+
15
+ def test_boolean
16
+ booleans = {
17
+ "\x00\x07".b => false,
18
+ "\x01\x07".b => true,
19
+ }
20
+ validate_type_decoding('booleans', booleans)
21
+ end
22
+
23
+ def test_bytes
24
+ tests = {
25
+ "\x83\xE4\xBA\xBA".b => '人'.b,
26
+ }
27
+ validate_type_decoding('bytes', tests)
28
+ end
29
+
30
+ def test_double
31
+ doubles = {
32
+ "\x68\x00\x00\x00\x00\x00\x00\x00\x00".b => 0.0,
33
+ "\x68\x3F\xE0\x00\x00\x00\x00\x00\x00".b => 0.5,
34
+ "\x68\x40\x09\x21\xFB\x54\x44\x2E\xEA".b => 3.14159265359,
35
+ "\x68\x40\x5E\xC0\x00\x00\x00\x00\x00".b => 123.0,
36
+ "\x68\x41\xD0\x00\x00\x00\x07\xF8\xF4".b => 1_073_741_824.12457,
37
+ "\x68\xBF\xE0\x00\x00\x00\x00\x00\x00".b => -0.5,
38
+ "\x68\xC0\x09\x21\xFB\x54\x44\x2E\xEA".b => -3.14159265359,
39
+ "\x68\xC1\xD0\x00\x00\x00\x07\xF8\xF4".b => -1_073_741_824.12457,
40
+ }
41
+ validate_type_decoding('double', doubles)
42
+ end
43
+
44
+ def test_float
45
+ floats = {
46
+ "\x04\x08\x00\x00\x00\x00".b => 0.0,
47
+ "\x04\x08\x3F\x80\x00\x00".b => 1.0,
48
+ "\x04\x08\x3F\x8C\xCC\xCD".b => 1.1,
49
+ "\x04\x08\x40\x48\xF5\xC3".b => 3.14,
50
+ "\x04\x08\x46\x1C\x3F\xF6".b => 9999.99,
51
+ "\x04\x08\xBF\x80\x00\x00".b => -1.0,
52
+ "\x04\x08\xBF\x8C\xCC\xCD".b => -1.1,
53
+ "\x04\x08\xC0\x48\xF5\xC3".b => -3.14,
54
+ "\x04\x08\xC6\x1C\x3F\xF6".b => -9999.99
55
+ }
56
+ validate_type_decoding('float', floats)
57
+ end
58
+
59
+ def test_int32
60
+ int32 = {
61
+ "\x00\x01".b => 0,
62
+ "\x04\x01\xff\xff\xff\xff".b => -1,
63
+ "\x01\x01\xff".b => 255,
64
+ "\x04\x01\xff\xff\xff\x01".b => -255,
65
+ "\x02\x01\x01\xf4".b => 500,
66
+ "\x04\x01\xff\xff\xfe\x0c".b => -500,
67
+ "\x02\x01\xff\xff".b => 65_535,
68
+ "\x04\x01\xff\xff\x00\x01".b => -65_535,
69
+ "\x03\x01\xff\xff\xff".b => 16_777_215,
70
+ "\x04\x01\xff\x00\x00\x01".b => -16_777_215,
71
+ "\x04\x01\x7f\xff\xff\xff".b => 2_147_483_647,
72
+ "\x04\x01\x80\x00\x00\x01".b => -2_147_483_647,
73
+ }
74
+ validate_type_decoding('int32', int32)
75
+ end
76
+
77
+ def test_map
78
+ maps = {
79
+ "\xe0".b => {},
80
+ "\xe1\x42\x65\x6e\x43\x46\x6f\x6f".b => {
81
+ 'en' => 'Foo'
82
+ },
83
+ "\xe2\x42\x65\x6e\x43\x46\x6f\x6f\x42\x7a\x68\x43\xe4\xba\xba".b => {
84
+ 'en' => 'Foo',
85
+ 'zh' => '人'
86
+ },
87
+ "\xe1\x44\x6e\x61\x6d\x65\xe2\x42\x65\x6e".b +
88
+ "\x43\x46\x6f\x6f\x42\x7a\x68\x43\xe4\xba\xba".b => {
89
+ 'name' => {
90
+ 'en' => 'Foo',
91
+ 'zh' => '人'
92
+ }
93
+ },
94
+ "\xe1\x49\x6c\x61\x6e\x67\x75\x61\x67\x65\x73".b +
95
+ "\x02\x04\x42\x65\x6e\x42\x7a\x68".b => {
96
+ 'languages' => %w[en zh]
97
+ },
98
+ MMDBUtil.make_metadata_map(28) => {
99
+ 'node_count' => 0,
100
+ 'record_size' => 28,
101
+ 'ip_version' => 4,
102
+ 'database_type' => 'test',
103
+ 'languages' => ['en'],
104
+ 'binary_format_major_version' => 2,
105
+ 'binary_format_minor_version' => 0,
106
+ 'build_epoch' => 0,
107
+ 'description' => 'hi',
108
+ },
109
+ }
110
+ validate_type_decoding('maps', maps)
111
+ end
112
+
113
+ def test_pointer
114
+ pointers = {
115
+ "\x20\x00".b => 0,
116
+ "\x20\x05".b => 5,
117
+ "\x20\x0a".b => 10,
118
+ "\x23\xff".b => 1023,
119
+ "\x28\x03\xc9".b => 3017,
120
+ "\x2f\xf7\xfb".b => 524_283,
121
+ "\x2f\xff\xff".b => 526_335,
122
+ "\x37\xf7\xf7\xfe".b => 134_217_726,
123
+ "\x37\xff\xff\xff".b => 134_744_063,
124
+ "\x38\x7f\xff\xff\xff".b => 2_147_483_647,
125
+ "\x38\xff\xff\xff\xff".b => 4_294_967_295,
126
+ }
127
+ validate_type_decoding('pointers', pointers)
128
+ end
129
+
130
+ # rubocop:disable Style/ClassVars
131
+ @@strings = {
132
+ "\x40".b => '',
133
+ "\x41\x31".b => '1',
134
+ "\x43\xE4\xBA\xBA".b => '人',
135
+ "\x5b\x31\x32\x33\x34".b +
136
+ "\x35\x36\x37\x38\x39\x30\x31\x32\x33\x34\x35".b +
137
+ "\x36\x37\x38\x39\x30\x31\x32\x33\x34\x35\x36\x37".b =>
138
+ '123456789012345678901234567',
139
+ "\x5c\x31\x32\x33\x34".b +
140
+ "\x35\x36\x37\x38\x39\x30\x31\x32\x33\x34\x35".b +
141
+ "\x36\x37\x38\x39\x30\x31\x32\x33\x34\x35\x36".b +
142
+ "\x37\x38".b => '1234567890123456789012345678',
143
+ "\x5d\x00\x31\x32\x33".b +
144
+ "\x34\x35\x36\x37\x38\x39\x30\x31\x32\x33\x34".b +
145
+ "\x35\x36\x37\x38\x39\x30\x31\x32\x33\x34\x35".b +
146
+ "\x36\x37\x38\x39".b => '12345678901234567890123456789',
147
+ "\x5d\x01\x31\x32\x33".b +
148
+ "\x34\x35\x36\x37\x38\x39\x30\x31\x32\x33\x34".b +
149
+ "\x35\x36\x37\x38\x39\x30\x31\x32\x33\x34\x35".b +
150
+ "\x36\x37\x38\x39\x30".b => '123456789012345678901234567890',
151
+ "\x5e\x00\xd7".b + "\x78".b * 500 => 'x' * 500,
152
+ "\x5e\x06\xb3".b + "\x78".b * 2000 => 'x' * 2000,
153
+ "\x5f\x00\x10\x53".b + "\x78".b * 70_000 => 'x' * 70_000,
154
+ }
155
+ # rubocop:enable Style/ClassVars
156
+
157
+ def test_string
158
+ values = validate_type_decoding('string', @@strings)
159
+ values.each do |s|
160
+ assert_equal(Encoding::UTF_8, s.encoding)
161
+ end
162
+ end
163
+
164
+ def test_uint16
165
+ uint16 = {
166
+ "\xa0".b => 0,
167
+ "\xa1\xff".b => 255,
168
+ "\xa2\x01\xf4".b => 500,
169
+ "\xa2\x2a\x78".b => 10_872,
170
+ "\xa2\xff\xff".b => 65_535,
171
+ }
172
+ validate_type_decoding('uint16', uint16)
173
+ end
174
+
175
+ def test_uint32
176
+ uint32 = {
177
+ "\xc0".b => 0,
178
+ "\xc1\xff".b => 255,
179
+ "\xc2\x01\xf4".b => 500,
180
+ "\xc2\x2a\x78".b => 10_872,
181
+ "\xc2\xff\xff".b => 65_535,
182
+ "\xc3\xff\xff\xff".b => 16_777_215,
183
+ "\xc4\xff\xff\xff\xff".b => 4_294_967_295,
184
+ }
185
+ validate_type_decoding('uint32', uint32)
186
+ end
187
+
188
+ def generate_large_uint(bits)
189
+ ctrl_byte = bits == 64 ? "\x02".b : "\x03".b
190
+ uints = {
191
+ "\x00".b + ctrl_byte => 0,
192
+ "\x02".b + ctrl_byte + "\x01\xf4".b => 500,
193
+ "\x02".b + ctrl_byte + "\x2a\x78".b => 10_872,
194
+ }
195
+ (bits / 8 + 1).times do |power|
196
+ expected = 2**(8 * power) - 1
197
+ input = [power].pack('C') + ctrl_byte + "\xff".b * power
198
+ uints[input] = expected
199
+ end
200
+ uints
201
+ end
202
+
203
+ def test_uint64
204
+ validate_type_decoding('uint64', generate_large_uint(64))
205
+ end
206
+
207
+ def test_uint128
208
+ validate_type_decoding('uint128', generate_large_uint(128))
209
+ end
210
+
211
+ def validate_type_decoding(type, tests)
212
+ values = []
213
+ tests.each do |input, expected|
214
+ values << check_decoding(type, input, expected)
215
+ end
216
+ values
217
+ end
218
+
219
+ def check_decoding(type, input, expected, name = nil)
220
+ name ||= expected
221
+
222
+ io = MaxMind::DB::MemoryReader.new(input, is_buffer: true)
223
+
224
+ pointer_base = 0
225
+ pointer_test = true
226
+ decoder = MaxMind::DB::Decoder.new(io, pointer_base,
227
+ pointer_test)
228
+
229
+ offset = 0
230
+ r = decoder.decode(offset)
231
+
232
+ if %w[float double].include?(type)
233
+ assert_in_delta(expected, r[0], 0.001, name)
234
+ else
235
+ assert_equal(expected, r[0], name)
236
+ end
237
+
238
+ io.close
239
+ r[0]
240
+ end
241
+ end
@@ -0,0 +1,415 @@
1
+ require 'maxmind/db'
2
+ require 'minitest/autorun'
3
+ require 'mmdb_util'
4
+
5
+ class ReaderTest < Minitest::Test # :nodoc:
6
+ def test_reader
7
+ modes = [
8
+ MaxMind::DB::MODE_FILE,
9
+ MaxMind::DB::MODE_MEMORY,
10
+ ]
11
+
12
+ modes.each do |mode|
13
+ [24, 28, 32].each do |record_size|
14
+ [4, 6].each do |ip_version|
15
+ filename = 'test/data/test-data/MaxMind-DB-test-ipv' +
16
+ ip_version.to_s + '-' + record_size.to_s + '.mmdb'
17
+ reader = MaxMind::DB.new(filename, mode: mode)
18
+ check_metadata(reader, ip_version, record_size)
19
+ if ip_version == 4
20
+ check_ipv4(reader, filename)
21
+ else
22
+ check_ipv6(reader, filename)
23
+ end
24
+ reader.close
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ def test_decoder
31
+ reader = MaxMind::DB.new(
32
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
33
+ )
34
+ record = reader.get('::1.1.1.0')
35
+ assert_equal([1, 2, 3], record['array'])
36
+ assert_equal(true, record['boolean'])
37
+ assert_equal("\x00\x00\x00*".b, record['bytes'])
38
+ assert_equal(42.123456, record['double'])
39
+ assert_in_delta(1.1, record['float'])
40
+ assert_equal(-268_435_456, record['int32'])
41
+ assert_equal(
42
+ {
43
+ 'mapX' => {
44
+ 'arrayX' => [7, 8, 9],
45
+ 'utf8_stringX' => 'hello',
46
+ },
47
+ },
48
+ record['map'],
49
+ )
50
+ assert_equal(100, record['uint16'])
51
+ assert_equal(268_435_456, record['uint32'])
52
+ assert_equal(1_152_921_504_606_846_976, record['uint64'])
53
+ assert_equal('unicode! ☯ - ♫', record['utf8_string'])
54
+ assert_equal(1_329_227_995_784_915_872_903_807_060_280_344_576, record['uint128'])
55
+ reader.close
56
+ end
57
+
58
+ def test_no_ipv4_search_tree
59
+ reader = MaxMind::DB.new(
60
+ 'test/data/test-data/MaxMind-DB-no-ipv4-search-tree.mmdb'
61
+ )
62
+ assert_equal('::0/64', reader.get('1.1.1.1'))
63
+ assert_equal('::0/64', reader.get('192.1.1.1'))
64
+ reader.close
65
+ end
66
+
67
+ def test_ipv6_address_in_ipv4_database
68
+ reader = MaxMind::DB.new(
69
+ 'test/data/test-data/MaxMind-DB-test-ipv4-24.mmdb'
70
+ )
71
+ e = assert_raises ArgumentError do
72
+ reader.get('2001::')
73
+ end
74
+ assert_equal(
75
+ 'Error looking up 2001::. You attempted to look up an IPv6 address in an IPv4-only database.',
76
+ e.message,
77
+ )
78
+ reader.close
79
+ end
80
+
81
+ def test_bad_ip_parameter
82
+ reader = MaxMind::DB.new('test/data/test-data/GeoIP2-City-Test.mmdb')
83
+ e = assert_raises ArgumentError do
84
+ reader.get(Object.new)
85
+ end
86
+ assert_equal(
87
+ 'address family must be specified', # Not great, but type is ok
88
+ e.message,
89
+ )
90
+ reader.close
91
+ end
92
+
93
+ def test_broken_database
94
+ reader = MaxMind::DB.new(
95
+ 'test/data/test-data/GeoIP2-City-Test-Broken-Double-Format.mmdb'
96
+ )
97
+ e = assert_raises MaxMind::DB::InvalidDatabaseError do
98
+ reader.get('2001:220::')
99
+ end
100
+ assert_equal(
101
+ 'The MaxMind DB file\'s data section contains bad data (unknown data type or corrupt data)',
102
+ e.message,
103
+ )
104
+ reader.close
105
+ end
106
+
107
+ def test_ip_validation
108
+ reader = MaxMind::DB.new(
109
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
110
+ )
111
+ e = assert_raises ArgumentError do
112
+ reader.get('not_ip')
113
+ end
114
+ assert_equal('invalid address', e.message)
115
+ reader.close
116
+ end
117
+
118
+ def test_missing_database
119
+ e = assert_raises SystemCallError do
120
+ MaxMind::DB.new('file-does-not-exist.mmdb')
121
+ end
122
+ assert(e.message.match(/No such file or directory/))
123
+ end
124
+
125
+ def test_nondatabase
126
+ e = assert_raises MaxMind::DB::InvalidDatabaseError do
127
+ MaxMind::DB.new('README.md')
128
+ end
129
+ assert_equal(
130
+ 'Metadata section not found. Is this a valid MaxMind DB file?',
131
+ e.message,
132
+ )
133
+ end
134
+
135
+ def test_too_many_constructor_args
136
+ e = assert_raises ArgumentError do
137
+ MaxMind::DB.new('README.md', {}, 'blah')
138
+ end
139
+ assert(e.message.match(/wrong number of arguments/))
140
+ end
141
+
142
+ def test_no_constructor_args
143
+ e = assert_raises ArgumentError do
144
+ MaxMind::DB.new
145
+ end
146
+ assert(e.message.match(/wrong number of arguments/))
147
+ end
148
+
149
+ def test_too_many_get_args
150
+ reader = MaxMind::DB.new(
151
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
152
+ )
153
+ e = assert_raises ArgumentError do
154
+ reader.get('1.1.1.1', 'blah')
155
+ end
156
+ assert(e.message.match(/wrong number of arguments/))
157
+ reader.close
158
+ end
159
+
160
+ def test_no_get_args
161
+ reader = MaxMind::DB.new(
162
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
163
+ )
164
+ e = assert_raises ArgumentError do
165
+ reader.get
166
+ end
167
+ assert(e.message.match(/wrong number of arguments/))
168
+ reader.close
169
+ end
170
+
171
+ def test_metadata_args
172
+ reader = MaxMind::DB.new(
173
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
174
+ )
175
+ e = assert_raises ArgumentError do
176
+ reader.metadata('hi')
177
+ end
178
+ assert(e.message.match(/wrong number of arguments/))
179
+ reader.close
180
+ end
181
+
182
+ def test_metadata_unknown_attribute
183
+ reader = MaxMind::DB.new(
184
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
185
+ )
186
+ e = assert_raises NoMethodError do
187
+ reader.metadata.what
188
+ end
189
+ assert(e.message.match(/undefined method `what'/))
190
+ reader.close
191
+ end
192
+
193
+ def test_close
194
+ reader = MaxMind::DB.new(
195
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
196
+ )
197
+ reader.close
198
+ end
199
+
200
+ def test_double_close
201
+ reader = MaxMind::DB.new(
202
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
203
+ )
204
+ reader.close
205
+ reader.close
206
+ end
207
+
208
+ def test_closed_get
209
+ reader = MaxMind::DB.new(
210
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
211
+ )
212
+ reader.close
213
+ e = assert_raises IOError do
214
+ reader.get('1.1.1.1')
215
+ end
216
+ assert_equal('closed stream', e.message)
217
+ end
218
+
219
+ def test_closed_metadata
220
+ reader = MaxMind::DB.new(
221
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
222
+ )
223
+ reader.close
224
+ assert_equal(
225
+ { 'en' => 'MaxMind DB Decoder Test database - contains every MaxMind DB data type' },
226
+ reader.metadata.description,
227
+ )
228
+ end
229
+
230
+ def test_threads
231
+ reader = MaxMind::DB.new(
232
+ 'test/data/test-data/GeoIP2-Domain-Test.mmdb'
233
+ )
234
+
235
+ num_threads = 16
236
+ num_lookups = 32
237
+ thread_lookups = []
238
+ num_threads.times do
239
+ thread_lookups << []
240
+ end
241
+
242
+ threads = []
243
+ num_threads.times do |i|
244
+ threads << Thread.new do
245
+ num_lookups.times do |j|
246
+ thread_lookups[i] << reader.get("65.115.240.#{j}")
247
+ thread_lookups[i] << reader.get("2a02:2770:3::#{j}")
248
+ end
249
+ end
250
+ end
251
+
252
+ threads.each(&:join)
253
+
254
+ thread_lookups.each do |a|
255
+ assert_equal(num_lookups * 2, a.length)
256
+ thread_lookups.each do |b|
257
+ assert_equal(a, b)
258
+ end
259
+ end
260
+
261
+ reader.close
262
+ end
263
+
264
+ # In these tests I am trying to exercise Reader#read_node directly. It is not
265
+ # too easy to test its behaviour with real databases, so construct dummy ones
266
+ # directly.
267
+ #
268
+ def test_read_node
269
+ tests = [
270
+ {
271
+ record_size: 24,
272
+ # Left record + right record
273
+ node_bytes: "\xab\xcd\xef".b + "\xbc\xfe\xfa".b,
274
+ left: 11_259_375,
275
+ right: 12_386_042,
276
+ check_left: "\x00\xab\xcd\xef".b.unpack('N')[0],
277
+ check_right: "\x00\xbc\xfe\xfa".b.unpack('N')[0],
278
+ },
279
+ {
280
+ record_size: 28,
281
+ # Left record (part) + middle byte + right record (part)
282
+ node_bytes: "\xab\xcd\xef".b + "\x12".b + "\xfd\xdc\xfa".b,
283
+ left: 28_036_591,
284
+ right: 50_191_610,
285
+ check_left: "\x01\xab\xcd\xef".b.unpack('N')[0],
286
+ check_right: "\x02\xfd\xdc\xfa".b.unpack('N')[0],
287
+ },
288
+ {
289
+ record_size: 32,
290
+ # Left record + right record
291
+ node_bytes: "\xab\xcd\xef\x12".b + "\xfd\xdc\xfa\x15".b,
292
+ left: 2_882_400_018,
293
+ right: 4_259_117_589,
294
+ check_left: "\xab\xcd\xef\x12".b.unpack('N')[0],
295
+ check_right: "\xfd\xdc\xfa\x15".b.unpack('N')[0],
296
+ },
297
+ ]
298
+
299
+ tests.each do |test|
300
+ buf = ''.b
301
+ buf += test[:node_bytes]
302
+
303
+ buf += "\x00".b * 16
304
+
305
+ buf += "\xab\xcd\xefMaxMind.com".b
306
+ buf += MMDBUtil.make_metadata_map(test[:record_size])
307
+
308
+ reader = MaxMind::DB.new(
309
+ buf, mode: MaxMind::DB::MODE_PARAM_IS_BUFFER
310
+ )
311
+
312
+ assert_equal(reader.metadata.record_size, test[:record_size])
313
+
314
+ assert_equal(test[:left], reader.send(:read_node, 0, 0))
315
+ assert_equal(test[:right], reader.send(:read_node, 0, 1))
316
+ assert_equal(test[:left], test[:check_left])
317
+ assert_equal(test[:right], test[:check_right])
318
+ end
319
+ end
320
+
321
+ def check_metadata(reader, ip_version, record_size)
322
+ metadata = reader.metadata
323
+
324
+ assert_equal(2, metadata.binary_format_major_version, 'major_version')
325
+ assert_equal(0, metadata.binary_format_minor_version, 'minor_version')
326
+ assert_operator(metadata.build_epoch, :>, 1_373_571_901, 'build_epoch')
327
+ assert_equal('Test', metadata.database_type, 'database_type')
328
+ assert_equal(
329
+ {
330
+ 'en' => 'Test Database',
331
+ 'zh' => 'Test Database Chinese',
332
+ },
333
+ metadata.description,
334
+ 'description',
335
+ )
336
+ assert_equal(ip_version, metadata.ip_version, 'ip_version')
337
+ assert_equal(%w[en zh], metadata.languages, 'languages')
338
+ assert_operator(metadata.node_count, :>, 36, 'node_count')
339
+ assert_equal(record_size, metadata.record_size, 'record_size')
340
+ end
341
+
342
+ def check_ipv4(reader, filename)
343
+ 6.times do |i|
344
+ address = "1.1.1.#{2**i}"
345
+ assert_equal(
346
+ { 'ip' => address },
347
+ reader.get(address),
348
+ "found expected data record for #{address} in #{filename}",
349
+ )
350
+ end
351
+
352
+ pairs = {
353
+ '1.1.1.3' => '1.1.1.2',
354
+ '1.1.1.5' => '1.1.1.4',
355
+ '1.1.1.7' => '1.1.1.4',
356
+ '1.1.1.9' => '1.1.1.8',
357
+ '1.1.1.15' => '1.1.1.8',
358
+ '1.1.1.17' => '1.1.1.16',
359
+ '1.1.1.31' => '1.1.1.16',
360
+ }
361
+ pairs.each do |key_address, value_address|
362
+ data = { 'ip' => value_address }
363
+ assert_equal(
364
+ data,
365
+ reader.get(key_address),
366
+ "found expected data record for #{key_address} in #{filename}",
367
+ )
368
+ end
369
+
370
+ ['1.1.1.33', '255.254.253.123'].each do |ip|
371
+ assert_nil(
372
+ reader.get(ip),
373
+ "#{ip} is not in #{filename}",
374
+ )
375
+ end
376
+ end
377
+
378
+ def check_ipv6(reader, filename)
379
+ subnets = [
380
+ '::1:ffff:ffff', '::2:0:0', '::2:0:40', '::2:0:50', '::2:0:58',
381
+ ]
382
+ subnets.each do |address|
383
+ assert_equal(
384
+ { 'ip' => address },
385
+ reader.get(address),
386
+ "found expected data record for #{address} in #{filename}",
387
+ )
388
+ end
389
+
390
+ pairs = {
391
+ '::2:0:1' => '::2:0:0',
392
+ '::2:0:33' => '::2:0:0',
393
+ '::2:0:39' => '::2:0:0',
394
+ '::2:0:41' => '::2:0:40',
395
+ '::2:0:49' => '::2:0:40',
396
+ '::2:0:52' => '::2:0:50',
397
+ '::2:0:57' => '::2:0:50',
398
+ '::2:0:59' => '::2:0:58',
399
+ }
400
+ pairs.each do |key_address, value_address|
401
+ assert_equal(
402
+ { 'ip' => value_address },
403
+ reader.get(key_address),
404
+ "found expected data record for #{key_address} in #{filename}",
405
+ )
406
+ end
407
+
408
+ ['1.1.1.33', '255.254.253.123', '89fa::'].each do |ip|
409
+ assert_nil(
410
+ reader.get(ip),
411
+ "#{ip} is not in #{filename}",
412
+ )
413
+ end
414
+ end
415
+ end