platphorm-maxmind-db 1.1.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.
@@ -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