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.
- 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
|