lib/sypex_geo/database.rb in sypex_geo-0.1.0 vs lib/sypex_geo/database.rb in sypex_geo-0.2.0

- old
+ new

@@ -3,161 +3,128 @@ module SypexGeo class DatabaseError < StandardError end class Database - attr_reader :version + TYPE_COUNTRY = 1 + TYPE_CITY = 3 + attr_reader :version, :time + def initialize(path) @file = File.open(path, 'rb') - setup! + parse_header + setup end - def lookup(ip, full = false) - if seek = search(ip) - read_location(seek, full) + def query(ip) + if position = search(ip) + Result.new(position, self) end end + def read_country(position) + @country_parser ||= Pack.new(@country_pack) + @country_parser.parse(@cities_db[position, @country_size]) + end + + def read_region(position) + @region_parser ||= Pack.new(@region_pack) + @region_parser.parse(@regions_db[position, @region_size]) + end + + def read_city(position) + @city_parser ||= Pack.new(@city_pack) + @city_parser.parse(@cities_db[position, @city_size]) + end + + def country? + @type == TYPE_COUNTRY + end + + def city? + @type == TYPE_CITY + end + def inspect "#<#{self.class}:0x#{object_id} @version=#{@version}>" end protected - def setup! + def parse_header if header = @file.read(40) id, @version, @time, @type, @charset, - @b_idx_len, @m_idx_len, @range, @db_items, @id_len, - @max_region, @max_city, @region_size, @city_size, - @max_country, @country_size, + @block_idx_size, @main_idx_size, @range, @db_records_count, @id_size, + @region_size, @city_size, @regions_db_size, @cities_db_size, + @country_size, @countries_db_size, @pack_size = header.unpack('a3CNCCCnnNCnnNNnNn') end raise DatabaseError.new, 'Wrong file format' unless id == 'SxG' + end + def setup @pack = @file.read(@pack_size).split("\0") - @b_idx_arr = @file.read(@b_idx_len * 4).unpack('N*') - @m_idx_arr = @file.read(@m_idx_len * 4).scan(/.{1,4}/m) + @country_pack, @region_pack, @city_pack = @pack - @block_len = 3 + @id_len - @db_begin = @file.tell - @regions_begin = @db_begin + @db_items * @block_len - @cities_begin = @regions_begin + @region_size + @block_idx = @file.read(@block_idx_size * 4).unpack('N*') + @main_idx = @file.read(@main_idx_size * 4).scan(/.{1,4}/m) + + @db_record_size = 3 + @id_size + @db = @file.read(@db_records_count * @db_record_size) + @regions_db = @file.read(@regions_db_size) if @regions_db_size > 0 + @cities_db = @file.read(@cities_db_size) if @cities_db_size > 0 end def search(ip) - ip1n = ip.to_i + octet = ip.to_i + return if octet == 0 or octet == 127 or octet >= @block_idx_size - return if ip1n == 0 or ip1n == 127 or ip1n >= 224 - + min, max = @block_idx[octet - 1], @block_idx[octet] + range = @range ipn = IPAddr.new(ip).hton - blocks_min, blocks_max = @b_idx_arr[ip1n - 1], @b_idx_arr[ip1n] - if blocks_max - blocks_min > @range - part = search_idx(ipn, blocks_min / @range, (blocks_max / @range) - 1) - min = part > 0 ? part * @range : 0 - max = part > @m_idx_len ? @db_items : (part + 1) * @range - min = blocks_min if min < blocks_min - max = blocks_max if max > blocks_max - else - min = blocks_min - max = blocks_max + if max - min > range + min = range * main_idx_search(ipn, min / range, (max / range) - 1) + max = min + range end - search_db(ipn, min, max) + db_search(ipn, min, max) end - def search_idx(ipn, min, max) - idx = @m_idx_arr + def main_idx_search(ipn, min, max) + idx = @main_idx - while max - min > 8 - offset = (min + max) >> 1 + while min < max + mid = (min + max) / 2 - if ipn > idx[offset] - min = offset + if ipn > idx[mid] + min = mid + 1 else - max = offset + max = mid end end - while ipn > idx[min] - break if min >= max - min += 1 - end - min end - def search_db(ipn, min, max) - len = max - min - @file.pos = @db_begin + min * @block_len - search_db_chunk(@file.read(len * @block_len), ipn, 0, len - 1) - end + def db_search(ipn, min, max) + db = @db + db_record_size = @db_record_size + octets = ipn[1, 3] - def search_db_chunk(data, ipn, min, max) - block_len = @block_len + while min < max + mid = (min + max) / 2 - if max - min > 1 - ipn = ipn[1, 3] - - while max - min > 8 - offset = (min + max) >> 1 - - if ipn > data[offset * block_len, 3] - min = offset - else - max = offset - end + if octets > db[mid * db_record_size, 3] + min = mid + 1 + else + max = mid end - - while ipn >= data[min * block_len, 3] - min += 1 - break if min >= max - end - else - min += 1 end - data[min * block_len - @id_len, @id_len].unpack('H*').first.hex - end - - def read_data(seek, limit, type) - @file.pos = (type == TYPE_REGION ? @regions_begin : @cities_begin) + seek - Pack.parse(@pack[type], @file.read(limit)) - end - - def read_country(seek) - read_data(seek, @max_country, TYPE_COUNTRY) - end - - def read_region(seek) - read_data(seek, @max_region, TYPE_REGION) - end - - def read_city(seek) - read_data(seek, @max_city, TYPE_CITY) - end - - def read_location(seek, full = false) - region = nil - city = nil - country = nil - - if seek < @country_size - country = read_country(seek) - elsif city = read_city(seek) - region_seek = city.delete(:region_seek) - country_id = city.delete(:country_id) - country = { id: country_id, iso: COUNTRY_CODES[country_id - 1] } - end - - if full and region_seek - region = read_region(region_seek) - country = read_country(region.delete(:country_seek)) - end - - { city: city, region: region, country: country } + db[min * db_record_size - @id_size, @id_size].unpack('H*')[0].hex end end end