platphorm-maxmind-db 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +25 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +15 -0
- data/LICENSE-APACHE +202 -0
- data/LICENSE-MIT +17 -0
- data/README.dev.md +34 -0
- data/README.md +63 -0
- data/Rakefile +9 -0
- data/bin/mmdb-benchmark.rb +64 -0
- data/lib/maxmind/db.rb +306 -0
- data/lib/maxmind/db/decoder.rb +235 -0
- data/lib/maxmind/db/errors.rb +10 -0
- data/lib/maxmind/db/file_reader.rb +40 -0
- data/lib/maxmind/db/memory_reader.rb +32 -0
- data/lib/maxmind/db/metadata.rb +89 -0
- data/platphorm-maxmind-db.gemspec +22 -0
- data/test/mmdb_util.rb +26 -0
- data/test/test_decoder.rb +243 -0
- data/test/test_reader.rb +539 -0
- metadata +68 -0
@@ -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
|