platphorm-maxmind-db 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MaxMind
4
+ class DB
5
+ # An InvalidDatabaseError means the {MaxMind
6
+ # DB}[https://maxmind.github.io/MaxMind-DB/] file is corrupt or invalid.
7
+ class InvalidDatabaseError < RuntimeError
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'maxmind/db/errors'
4
+
5
+ module MaxMind
6
+ class DB
7
+ # @!visibility private
8
+ class FileReader
9
+ def initialize(filename)
10
+ @fh = File.new(filename, 'rb')
11
+ @size = @fh.size
12
+ @mutex = Mutex.new
13
+ end
14
+
15
+ attr_reader :size
16
+
17
+ def close
18
+ @fh.close
19
+ end
20
+
21
+ def read(offset, size)
22
+ return ''.b if size == 0
23
+
24
+ # When we support only Ruby 2.5+, remove this and require pread.
25
+ if @fh.respond_to?(:pread)
26
+ buf = @fh.pread(size, offset)
27
+ else
28
+ @mutex.synchronize do
29
+ @fh.seek(offset, IO::SEEK_SET)
30
+ buf = @fh.read(size)
31
+ end
32
+ end
33
+
34
+ raise InvalidDatabaseError, 'The MaxMind DB file contains bad data' if buf.nil? || buf.length != size
35
+
36
+ buf
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MaxMind
4
+ class DB
5
+ # @!visibility private
6
+ class MemoryReader
7
+ def initialize(filename, options = {})
8
+ if options[:is_buffer]
9
+ @buf = filename
10
+ @size = @buf.length
11
+ return
12
+ end
13
+
14
+ @buf = File.read(filename, mode: 'rb').freeze
15
+ @size = @buf.length
16
+ end
17
+
18
+ attr_reader :size
19
+
20
+ # Override to not show @buf in inspect to avoid showing it in irb.
21
+ def inspect
22
+ "#<#{self.class.name}:0x#{self.class.object_id.to_s(16)}, @size=#{@size.inspect}>"
23
+ end
24
+
25
+ def close; end
26
+
27
+ def read(offset, size)
28
+ @buf[offset, size]
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MaxMind
4
+ class DB
5
+ # Metadata holds metadata about a {MaxMind
6
+ # DB}[https://maxmind.github.io/MaxMind-DB/] file. See
7
+ # https://maxmind.github.io/MaxMind-DB/#database-metadata for the
8
+ # specification.
9
+ class Metadata
10
+ # The number of nodes in the database.
11
+ #
12
+ # @return [Integer]
13
+ attr_reader :node_count
14
+
15
+ # The bit size of a record in the search tree.
16
+ #
17
+ # @return [Integer]
18
+ attr_reader :record_size
19
+
20
+ # The IP version of the data in the database. A value of 4 means the
21
+ # database only supports IPv4. A database with a value of 6 may support
22
+ # both IPv4 and IPv6 lookups.
23
+ #
24
+ # @return [Integer]
25
+ attr_reader :ip_version
26
+
27
+ # A string identifying the database type. e.g., "GeoIP2-City".
28
+ #
29
+ # @return [String]
30
+ attr_reader :database_type
31
+
32
+ # An array of locale codes supported by the database.
33
+ #
34
+ # @return [Array<String>]
35
+ attr_reader :languages
36
+
37
+ # The major version number of the binary format used when creating the
38
+ # database.
39
+ #
40
+ # @return [Integer]
41
+ attr_reader :binary_format_major_version
42
+
43
+ # The minor version number of the binary format used when creating the
44
+ # database.
45
+ #
46
+ # @return [Integer]
47
+ attr_reader :binary_format_minor_version
48
+
49
+ # The Unix epoch for the build time of the database.
50
+ #
51
+ # @return [Integer]
52
+ attr_reader :build_epoch
53
+
54
+ # A hash from locales to text descriptions of the database.
55
+ #
56
+ # @return [Hash<String, String>]
57
+ attr_reader :description
58
+
59
+ # +m+ is a hash representing the metadata map.
60
+ #
61
+ # @!visibility private
62
+ def initialize(map)
63
+ @node_count = map['node_count']
64
+ @record_size = map['record_size']
65
+ @ip_version = map['ip_version']
66
+ @database_type = map['database_type']
67
+ @languages = map['languages']
68
+ @binary_format_major_version = map['binary_format_major_version']
69
+ @binary_format_minor_version = map['binary_format_minor_version']
70
+ @build_epoch = map['build_epoch']
71
+ @description = map['description']
72
+ end
73
+
74
+ # The size of a node in bytes.
75
+ #
76
+ # @return [Integer]
77
+ def node_byte_size
78
+ @record_size / 4
79
+ end
80
+
81
+ # The size of the search tree in bytes.
82
+ #
83
+ # @return [Integer]
84
+ def search_tree_size
85
+ @node_count * node_byte_size
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |s|
4
+ s.authors = ['William Storey']
5
+ s.files = Dir['**/*']
6
+ s.name = 'platphorm-maxmind-db'
7
+ s.summary = 'A gem for reading MaxMind DB files.'
8
+ s.version = '1.1.0'
9
+
10
+ s.description = 'A gem for reading MaxMind DB files. MaxMind DB is a binary file format that stores data indexed by IP address subnets (IPv4 or IPv6).'
11
+ s.email = 'support@maxmind.com'
12
+ s.homepage = 'https://github.com/maxmind/MaxMind-DB-Reader-ruby'
13
+ s.licenses = ['Apache-2.0', 'MIT']
14
+ s.metadata = {
15
+ 'bug_tracker_uri' => 'https://github.com/maxmind/MaxMind-DB-Reader-ruby/issues',
16
+ 'changelog_uri' => 'https://github.com/maxmind/MaxMind-DB-Reader-ruby/blob/master/CHANGELOG.md',
17
+ 'documentation_uri' => 'https://github.com/maxmind/MaxMind-DB-Reader-ruby',
18
+ 'homepage_uri' => 'https://github.com/maxmind/MaxMind-DB-Reader-ruby',
19
+ 'source_code_uri' => 'https://github.com/maxmind/MaxMind-DB-Reader-ruby',
20
+ }
21
+ s.required_ruby_version = '>= 2.2.0'
22
+ end
data/test/mmdb_util.rb ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MMDBUtil
4
+ def self.make_metadata_map(record_size)
5
+ # Map
6
+ "\xe9".b +
7
+ # node_count => 0
8
+ "\x4anode_count\xc0".b +
9
+ # record_size => 28 would be \xa1\x1c
10
+ "\x4brecord_size\xa1".b + record_size.chr.b +
11
+ # ip_version => 4
12
+ "\x4aip_version\xa1\x04".b +
13
+ # database_type => 'test'
14
+ "\x4ddatabase_type\x44test".b +
15
+ # languages => ['en']
16
+ "\x49languages\x01\x04\x42en".b +
17
+ # binary_format_major_version => 2
18
+ "\x5bbinary_format_major_version\xa1\x02".b +
19
+ # binary_format_minor_version => 0
20
+ "\x5bbinary_format_minor_version\xa0".b +
21
+ # build_epoch => 0
22
+ "\x4bbuild_epoch\x00\x02".b +
23
+ # description => 'hi'
24
+ "\x4bdescription\x42hi".b
25
+ end
26
+ end
@@ -0,0 +1,243 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'maxmind/db'
4
+ require 'minitest/autorun'
5
+ require 'mmdb_util'
6
+
7
+ class DecoderTest < Minitest::Test
8
+ def test_arrays
9
+ arrays = {
10
+ "\x00\x04".b => [],
11
+ "\x01\x04\x43\x46\x6f\x6f".b => ['Foo'],
12
+ "\x02\x04\x43\x46\x6f\x6f\x43\xe4\xba\xba".b => %w[Foo 人],
13
+ }
14
+ validate_type_decoding('arrays', arrays)
15
+ end
16
+
17
+ def test_boolean
18
+ booleans = {
19
+ "\x00\x07".b => false,
20
+ "\x01\x07".b => true,
21
+ }
22
+ validate_type_decoding('booleans', booleans)
23
+ end
24
+
25
+ def test_bytes
26
+ tests = {
27
+ "\x83\xE4\xBA\xBA".b => '人'.b,
28
+ }
29
+ validate_type_decoding('bytes', tests)
30
+ end
31
+
32
+ def test_double
33
+ doubles = {
34
+ "\x68\x00\x00\x00\x00\x00\x00\x00\x00".b => 0.0,
35
+ "\x68\x3F\xE0\x00\x00\x00\x00\x00\x00".b => 0.5,
36
+ "\x68\x40\x09\x21\xFB\x54\x44\x2E\xEA".b => 3.14159265359,
37
+ "\x68\x40\x5E\xC0\x00\x00\x00\x00\x00".b => 123.0,
38
+ "\x68\x41\xD0\x00\x00\x00\x07\xF8\xF4".b => 1_073_741_824.12457,
39
+ "\x68\xBF\xE0\x00\x00\x00\x00\x00\x00".b => -0.5,
40
+ "\x68\xC0\x09\x21\xFB\x54\x44\x2E\xEA".b => -3.14159265359,
41
+ "\x68\xC1\xD0\x00\x00\x00\x07\xF8\xF4".b => -1_073_741_824.12457,
42
+ }
43
+ validate_type_decoding('double', doubles)
44
+ end
45
+
46
+ def test_float
47
+ floats = {
48
+ "\x04\x08\x00\x00\x00\x00".b => 0.0,
49
+ "\x04\x08\x3F\x80\x00\x00".b => 1.0,
50
+ "\x04\x08\x3F\x8C\xCC\xCD".b => 1.1,
51
+ "\x04\x08\x40\x48\xF5\xC3".b => 3.14,
52
+ "\x04\x08\x46\x1C\x3F\xF6".b => 9999.99,
53
+ "\x04\x08\xBF\x80\x00\x00".b => -1.0,
54
+ "\x04\x08\xBF\x8C\xCC\xCD".b => -1.1,
55
+ "\x04\x08\xC0\x48\xF5\xC3".b => -3.14,
56
+ "\x04\x08\xC6\x1C\x3F\xF6".b => -9999.99
57
+ }
58
+ validate_type_decoding('float', floats)
59
+ end
60
+
61
+ def test_int32
62
+ int32 = {
63
+ "\x00\x01".b => 0,
64
+ "\x04\x01\xff\xff\xff\xff".b => -1,
65
+ "\x01\x01\xff".b => 255,
66
+ "\x04\x01\xff\xff\xff\x01".b => -255,
67
+ "\x02\x01\x01\xf4".b => 500,
68
+ "\x04\x01\xff\xff\xfe\x0c".b => -500,
69
+ "\x02\x01\xff\xff".b => 65_535,
70
+ "\x04\x01\xff\xff\x00\x01".b => -65_535,
71
+ "\x03\x01\xff\xff\xff".b => 16_777_215,
72
+ "\x04\x01\xff\x00\x00\x01".b => -16_777_215,
73
+ "\x04\x01\x7f\xff\xff\xff".b => 2_147_483_647,
74
+ "\x04\x01\x80\x00\x00\x01".b => -2_147_483_647,
75
+ }
76
+ validate_type_decoding('int32', int32)
77
+ end
78
+
79
+ def test_map
80
+ maps = {
81
+ "\xe0".b => {},
82
+ "\xe1\x42\x65\x6e\x43\x46\x6f\x6f".b => {
83
+ 'en' => 'Foo'
84
+ },
85
+ "\xe2\x42\x65\x6e\x43\x46\x6f\x6f\x42\x7a\x68\x43\xe4\xba\xba".b => {
86
+ 'en' => 'Foo',
87
+ 'zh' => '人'
88
+ },
89
+ "\xe1\x44\x6e\x61\x6d\x65\xe2\x42\x65\x6e".b +
90
+ "\x43\x46\x6f\x6f\x42\x7a\x68\x43\xe4\xba\xba".b => {
91
+ 'name' => {
92
+ 'en' => 'Foo',
93
+ 'zh' => '人'
94
+ }
95
+ },
96
+ "\xe1\x49\x6c\x61\x6e\x67\x75\x61\x67\x65\x73".b +
97
+ "\x02\x04\x42\x65\x6e\x42\x7a\x68".b => {
98
+ 'languages' => %w[en zh]
99
+ },
100
+ MMDBUtil.make_metadata_map(28) => {
101
+ 'node_count' => 0,
102
+ 'record_size' => 28,
103
+ 'ip_version' => 4,
104
+ 'database_type' => 'test',
105
+ 'languages' => ['en'],
106
+ 'binary_format_major_version' => 2,
107
+ 'binary_format_minor_version' => 0,
108
+ 'build_epoch' => 0,
109
+ 'description' => 'hi',
110
+ },
111
+ }
112
+ validate_type_decoding('maps', maps)
113
+ end
114
+
115
+ def test_pointer
116
+ pointers = {
117
+ "\x20\x00".b => 0,
118
+ "\x20\x05".b => 5,
119
+ "\x20\x0a".b => 10,
120
+ "\x23\xff".b => 1023,
121
+ "\x28\x03\xc9".b => 3017,
122
+ "\x2f\xf7\xfb".b => 524_283,
123
+ "\x2f\xff\xff".b => 526_335,
124
+ "\x37\xf7\xf7\xfe".b => 134_217_726,
125
+ "\x37\xff\xff\xff".b => 134_744_063,
126
+ "\x38\x7f\xff\xff\xff".b => 2_147_483_647,
127
+ "\x38\xff\xff\xff\xff".b => 4_294_967_295,
128
+ }
129
+ validate_type_decoding('pointers', pointers)
130
+ end
131
+
132
+ # rubocop:disable Style/ClassVars
133
+ @@strings = {
134
+ "\x40".b => '',
135
+ "\x41\x31".b => '1',
136
+ "\x43\xE4\xBA\xBA".b => '人',
137
+ "\x5b\x31\x32\x33\x34".b +
138
+ "\x35\x36\x37\x38\x39\x30\x31\x32\x33\x34\x35".b +
139
+ "\x36\x37\x38\x39\x30\x31\x32\x33\x34\x35\x36\x37".b =>
140
+ '123456789012345678901234567',
141
+ "\x5c\x31\x32\x33\x34".b +
142
+ "\x35\x36\x37\x38\x39\x30\x31\x32\x33\x34\x35".b +
143
+ "\x36\x37\x38\x39\x30\x31\x32\x33\x34\x35\x36".b +
144
+ "\x37\x38".b => '1234567890123456789012345678',
145
+ "\x5d\x00\x31\x32\x33".b +
146
+ "\x34\x35\x36\x37\x38\x39\x30\x31\x32\x33\x34".b +
147
+ "\x35\x36\x37\x38\x39\x30\x31\x32\x33\x34\x35".b +
148
+ "\x36\x37\x38\x39".b => '12345678901234567890123456789',
149
+ "\x5d\x01\x31\x32\x33".b +
150
+ "\x34\x35\x36\x37\x38\x39\x30\x31\x32\x33\x34".b +
151
+ "\x35\x36\x37\x38\x39\x30\x31\x32\x33\x34\x35".b +
152
+ "\x36\x37\x38\x39\x30".b => '123456789012345678901234567890',
153
+ "\x5e\x00\xd7".b + "\x78".b * 500 => 'x' * 500,
154
+ "\x5e\x06\xb3".b + "\x78".b * 2000 => 'x' * 2000,
155
+ "\x5f\x00\x10\x53".b + "\x78".b * 70_000 => 'x' * 70_000,
156
+ }
157
+ # rubocop:enable Style/ClassVars
158
+
159
+ def test_string
160
+ values = validate_type_decoding('string', @@strings)
161
+ values.each do |s|
162
+ assert_equal(Encoding::UTF_8, s.encoding)
163
+ end
164
+ end
165
+
166
+ def test_uint16
167
+ uint16 = {
168
+ "\xa0".b => 0,
169
+ "\xa1\xff".b => 255,
170
+ "\xa2\x01\xf4".b => 500,
171
+ "\xa2\x2a\x78".b => 10_872,
172
+ "\xa2\xff\xff".b => 65_535,
173
+ }
174
+ validate_type_decoding('uint16', uint16)
175
+ end
176
+
177
+ def test_uint32
178
+ uint32 = {
179
+ "\xc0".b => 0,
180
+ "\xc1\xff".b => 255,
181
+ "\xc2\x01\xf4".b => 500,
182
+ "\xc2\x2a\x78".b => 10_872,
183
+ "\xc2\xff\xff".b => 65_535,
184
+ "\xc3\xff\xff\xff".b => 16_777_215,
185
+ "\xc4\xff\xff\xff\xff".b => 4_294_967_295,
186
+ }
187
+ validate_type_decoding('uint32', uint32)
188
+ end
189
+
190
+ def generate_large_uint(bits)
191
+ ctrl_byte = bits == 64 ? "\x02".b : "\x03".b
192
+ uints = {
193
+ "\x00".b + ctrl_byte => 0,
194
+ "\x02".b + ctrl_byte + "\x01\xf4".b => 500,
195
+ "\x02".b + ctrl_byte + "\x2a\x78".b => 10_872,
196
+ }
197
+ (bits / 8 + 1).times do |power|
198
+ expected = 2**(8 * power) - 1
199
+ input = [power].pack('C') + ctrl_byte + "\xff".b * power
200
+ uints[input] = expected
201
+ end
202
+ uints
203
+ end
204
+
205
+ def test_uint64
206
+ validate_type_decoding('uint64', generate_large_uint(64))
207
+ end
208
+
209
+ def test_uint128
210
+ validate_type_decoding('uint128', generate_large_uint(128))
211
+ end
212
+
213
+ def validate_type_decoding(type, tests)
214
+ values = []
215
+ tests.each do |input, expected|
216
+ values << check_decoding(type, input, expected)
217
+ end
218
+ values
219
+ end
220
+
221
+ def check_decoding(type, input, expected, name = nil)
222
+ name ||= expected
223
+
224
+ io = MaxMind::DB::MemoryReader.new(input, is_buffer: true)
225
+
226
+ pointer_base = 0
227
+ pointer_test = true
228
+ decoder = MaxMind::DB::Decoder.new(io, pointer_base,
229
+ pointer_test)
230
+
231
+ offset = 0
232
+ r = decoder.decode(offset)
233
+
234
+ if %w[float double].include?(type)
235
+ assert_in_delta(expected, r[0], 0.001, name)
236
+ else
237
+ assert_equal(expected, r[0], name)
238
+ end
239
+
240
+ io.close
241
+ r[0]
242
+ end
243
+ end