maxmind-db 1.0.0.beta

Sign up to get free protection for your applications and to get access to all the features.
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