fh = fopen($db_file, 'rb'); // Сначала убеждаемся, что есть файл базы данных $header = fread($this->fh, 40); // В версии 2.2 заголовок увеличился на 8 байт if(substr($header, 0, 3) != 'SxG') die("Can't open {$db_file}\n"); $info = unpack('Cver/Ntime/Ctype/Ccharset/Cb_idx_len/nm_idx_len/nrange/Ndb_items/Cid_len/nmax_region/nmax_city/Nregion_size/Ncity_size/nmax_country/Ncountry_size/npack_size', substr($header, 3)); if($info['b_idx_len'] * $info['m_idx_len'] * $info['range'] * $info['db_items'] * $info['time'] * $info['id_len'] == 0) die("Wrong file format {$db_file}\n"); $this->range = $info['range']; $this->b_idx_len = $info['b_idx_len']; $this->m_idx_len = $info['m_idx_len']; $this->db_items = $info['db_items']; $this->id_len = $info['id_len']; $this->block_len = 3 + $this->id_len; $this->max_region = $info['max_region']; $this->max_city = $info['max_city']; $this->max_country = $info['max_country']; $this->country_size= $info['country_size']; $this->batch_mode = $type & SXGEO_BATCH; $this->memory_mode = $type & SXGEO_MEMORY; $this->pack = $info['pack_size'] ? explode("\0", fread($this->fh, $info['pack_size'])) : ''; $this->b_idx_str = fread($this->fh, $info['b_idx_len'] * 4); $this->m_idx_str = fread($this->fh, $info['m_idx_len'] * 4); $this->db_begin = ftell($this->fh); if ($this->batch_mode) { $this->b_idx_arr = array_values(unpack("N*", $this->b_idx_str)); // Быстрее в 5 раз, чем с циклом unset ($this->b_idx_str); $this->m_idx_arr = str_split($this->m_idx_str, 4); // Быстрее в 5 раз чем с циклом unset ($this->m_idx_str); } if ($this->memory_mode) { $this->db = fread($this->fh, $this->db_items * $this->block_len); $this->regions_db = $info['region_size'] > 0 ? fread($this->fh, $info['region_size']) : ''; $this->cities_db = $info['city_size'] > 0 ? fread($this->fh, $info['city_size']) : ''; } $this->info = $info; $this->info['regions_begin'] = $this->db_begin + $this->db_items * $this->block_len; $this->info['cities_begin'] = $this->info['regions_begin'] + $info['region_size']; } protected function search_idx($ipn, $min, $max){ if($this->batch_mode){ while($max - $min > 8){ $offset = ($min + $max) >> 1; if ($ipn > $this->m_idx_arr[$offset]) $min = $offset; else $max = $offset; } while ($ipn > $this->m_idx_arr[$min] && $min++ < $max){}; } else { while($max - $min > 8){ $offset = ($min + $max) >> 1; if ($ipn > substr($this->m_idx_str, $offset*4, 4)) $min = $offset; else $max = $offset; } while ($ipn > substr($this->m_idx_str, $min*4, 4) && $min++ < $max){}; } return $min; } protected function search_db($str, $ipn, $min, $max){ if($max - $min > 1) { $ipn = substr($ipn, 1); while($max - $min > 8){ $offset = ($min + $max) >> 1; if ($ipn > substr($str, $offset * $this->block_len, 3)) $min = $offset; else $max = $offset; } while ($ipn >= substr($str, $min * $this->block_len, 3) && ++$min < $max){}; } else { $min++; } return hexdec(bin2hex(substr($str, $min * $this->block_len - $this->id_len, $this->id_len))); } public function get_num($ip){ $ip1n = (int)$ip; // Первый байт if($ip1n == 0 || $ip1n == 10 || $ip1n == 127 || $ip1n >= $this->b_idx_len || false === ($ipn = ip2long($ip))) return false; $ipn = pack('N', $ipn); $this->ip1c = chr($ip1n); // Находим блок данных в индексе первых байт if ($this->batch_mode){ $blocks = array('min' => $this->b_idx_arr[$ip1n-1], 'max' => $this->b_idx_arr[$ip1n]); } else { $blocks = unpack("Nmin/Nmax", substr($this->b_idx_str, ($ip1n - 1) * 4, 8)); } if ($blocks['max'] - $blocks['min'] > $this->range){ // Ищем блок в основном индексе $part = $this->search_idx($ipn, floor($blocks['min'] / $this->range), floor($blocks['max'] / $this->range)-1); // Нашли номер блока в котором нужно искать IP, теперь находим нужный блок в БД $min = $part > 0 ? $part * $this->range : 0; $max = $part > $this->m_idx_len ? $this->db_items : ($part+1) * $this->range; // Нужно проверить чтобы блок не выходил за пределы блока первого байта if($min < $blocks['min']) $min = $blocks['min']; if($max > $blocks['max']) $max = $blocks['max']; } else { $min = $blocks['min']; $max = $blocks['max']; } $len = $max - $min; // Находим нужный диапазон в БД if ($this->memory_mode) { return $this->search_db($this->db, $ipn, $min, $max); } else { fseek($this->fh, $this->db_begin + $min * $this->block_len); return $this->search_db(fread($this->fh, $len * $this->block_len), $ipn, 0, $len); } } protected function readData($seek, $max, $type){ $raw = ''; if($seek && $max) { if ($this->memory_mode) { $raw = substr($type == 1 ? $this->regions_db : $this->cities_db, $seek, $max); } else { fseek($this->fh, $this->info[$type == 1 ? 'regions_begin' : 'cities_begin'] + $seek); $raw = fread($this->fh, $max); } } return $this->unpack($this->pack[$type], $raw); } protected function parseCity($seek, $full = false){ if(!$this->pack) return false; $only_country = false; if($seek < $this->country_size){ $country = $this->readData($seek, $this->max_country, 0); $city = $this->unpack($this->pack[2]); $city['lat'] = $country['lat']; $city['lon'] = $country['lon']; $only_country = true; } else { $city = $this->readData($seek, $this->max_city, 2); $country = array('id' => $city['country_id'], 'iso' => $this->id2iso[$city['country_id']]); unset($city['country_id']); } if($full) { $region = $this->readData($city['region_seek'], $this->max_region, 1); if(!$only_country) $country = $this->readData($region['country_seek'], $this->max_country, 0); unset($city['region_seek']); unset($region['country_seek']); return array('city' => $city, 'region' => $region, 'country' => $country); } else { unset($city['region_seek']); return array('city' => $city, 'country' => array('id' => $country['id'], 'iso' => $country['iso'])); } } protected function unpack($pack, $item = ''){ $unpacked = array(); $empty = empty($item); $pack = explode('/', $pack); $pos = 0; foreach($pack AS $p){ list($type, $name) = explode(':', $p); $type0 = $type{0}; if($empty) { $unpacked[$name] = $type0 == 'b' || $type0 == 'c' ? '' : 0; continue; } switch($type0){ case 't': case 'T': $l = 1; break; case 's': case 'n': case 'S': $l = 2; break; case 'm': case 'M': $l = 3; break; case 'd': $l = 8; break; case 'c': $l = (int)substr($type, 1); break; case 'b': $l = strpos($item, "\0", $pos)-$pos; break; default: $l = 4; } $val = substr($item, $pos, $l); switch($type0){ case 't': $v = unpack('c', $val); break; case 'T': $v = unpack('C', $val); break; case 's': $v = unpack('s', $val); break; case 'S': $v = unpack('S', $val); break; case 'm': $v = unpack('l', $val . (ord($val{2}) >> 7 ? "\xff" : "\0")); break; case 'M': $v = unpack('L', $val . "\0"); break; case 'i': $v = unpack('l', $val); break; case 'I': $v = unpack('L', $val); break; case 'f': $v = unpack('f', $val); break; case 'd': $v = unpack('d', $val); break; case 'n': $v = current(unpack('s', $val)) / pow(10, $type{1}); break; case 'N': $v = current(unpack('l', $val)) / pow(10, $type{1}); break; case 'c': $v = rtrim($val, ' '); break; case 'b': $v = $val; $l++; break; } $pos += $l; $unpacked[$name] = is_array($v) ? current($v) : $v; } return $unpacked; } public function get($ip){ return $this->max_city ? $this->getCity($ip) : $this->getCountry($ip); } public function getCountry($ip){ if($this->max_city) { $tmp = $this->parseCity($this->get_num($ip)); return $tmp['country']['iso']; } else return $this->id2iso[$this->get_num($ip)]; } public function getCountryId($ip){ if($this->max_city) { $tmp = $this->parseCity($this->get_num($ip)); return $tmp['country']['id']; } else return $this->get_num($ip); } public function getCity($ip){ $seek = $this->get_num($ip); return $seek ? $this->parseCity($seek) : false; } public function getCityFull($ip){ $seek = $this->get_num($ip); return $seek ? $this->parseCity($seek, 1) : false; } public function about(){ $charset = array('utf-8', 'latin1', 'cp1251'); $types = array('n/a', 'SxGeo Country', 'SxGeo City RU', 'SxGeo City EN', 'SxGeo City', 'SxGeo City Max RU', 'SxGeo City Max EN', 'SxGeo City Max'); return array( 'Created' => date('Y.m.d', $this->info['time']), 'Timestamp' => $this->info['time'], 'Charset' => $charset[$this->info['charset']], 'Type' => $types[$this->info['type']], 'Byte Index' => $this->b_idx_len, 'Main Index' => $this->m_idx_len, 'Blocks In Index Item' => $this->range, 'IP Blocks' => $this->db_items, 'Block Size' => $this->block_len, 'City' => array( 'Max Length' => $this->max_city, 'Total Size' => $this->info['city_size'], ), 'Region' => array( 'Max Length' => $this->max_region, 'Total Size' => $this->info['region_size'], ), 'Country' => array( 'Max Length' => $this->max_country, 'Total Size' => $this->info['country_size'], ), ); } }