From 95c129c7d17ccce85d0dd016c29bd09c1a5c9aab Mon Sep 17 00:00:00 2001 From: lion Date: Sun, 14 Sep 2025 23:14:42 +0800 Subject: [PATCH] PHP IPv6 supporting --- binding/php/XdbSearcher.class.php | 349 --------------------- binding/php/bench_test.php | 48 +-- binding/php/search_test.php | 35 ++- binding/php/util_test.php | 57 ---- binding/php/xdb/Searcher.class.php | 486 +++++++++++++++++++++++++++++ binding/php/xdb/util_test.php | 118 +++++++ binding/php5_ext/ReadMe.md | 7 - binding/php7_ext/ReadMe.md | 7 - 8 files changed, 652 insertions(+), 455 deletions(-) delete mode 100644 binding/php/XdbSearcher.class.php delete mode 100644 binding/php/util_test.php create mode 100644 binding/php/xdb/Searcher.class.php create mode 100644 binding/php/xdb/util_test.php delete mode 100644 binding/php5_ext/ReadMe.md delete mode 100644 binding/php7_ext/ReadMe.md diff --git a/binding/php/XdbSearcher.class.php b/binding/php/XdbSearcher.class.php deleted file mode 100644 index f743198..0000000 --- a/binding/php/XdbSearcher.class.php +++ /dev/null @@ -1,349 +0,0 @@ - -// @Date 2022/06/21 - -class XdbSearcher -{ - const HeaderInfoLength = 256; - const VectorIndexRows = 256; - const VectorIndexCols = 256; - const VectorIndexSize = 8; - const SegmentIndexSize = 14; - - // xdb file handle - private $handle = null; - - // header info - private $header = null; - private $ioCount = 0; - - // vector index in binary string. - // string decode will be faster than the map based Array. - private $vectorIndex = null; - - // xdb content buffer - private $contentBuff = null; - - // --- - // static function to create searcher - - /** - * @throws Exception - */ - public static function newWithFileOnly($dbFile) { - return new XdbSearcher($dbFile, null, null); - } - - /** - * @throws Exception - */ - public static function newWithVectorIndex($dbFile, $vIndex) { - return new XdbSearcher($dbFile, $vIndex); - } - - /** - * @throws Exception - */ - public static function newWithBuffer($cBuff) { - return new XdbSearcher(null, null, $cBuff); - } - - // --- End of static creator - - /** - * initialize the xdb searcher - * @throws Exception - */ - function __construct($dbFile, $vectorIndex=null, $cBuff=null) { - // check the content buffer first - if ($cBuff != null) { - $this->vectorIndex = null; - $this->contentBuff = $cBuff; - } else { - // open the xdb binary file - $this->handle = fopen($dbFile, "r"); - if ($this->handle === false) { - throw new Exception("failed to open xdb file '%s'", $dbFile); - } - - $this->vectorIndex = $vectorIndex; - } - } - - function close() { - if ($this->handle != null) { - fclose($this->handle); - } - } - - function getIOCount() { - return $this->ioCount; - } - - /** - * find the region info for the specified ip address - * @throws Exception - */ - function search($ip) { - // check and convert the sting ip to a 4-bytes long - if (is_string($ip)) { - $t = self::ip2long($ip); - if ($t === null) { - throw new Exception("invalid ip address `$ip`"); - } - $ip = $t; - } - - // reset the global counter - $this->ioCount = 0; - - // locate the segment index block based on the vector index - $il0 = ($ip >> 24) & 0xFF; - $il1 = ($ip >> 16) & 0xFF; - $idx = $il0 * self::VectorIndexCols * self::VectorIndexSize + $il1 * self::VectorIndexSize; - if ($this->vectorIndex != null) { - $sPtr = self::getLong($this->vectorIndex, $idx); - $ePtr = self::getLong($this->vectorIndex, $idx + 4); - } else if ($this->contentBuff != null) { - $sPtr = self::getLong($this->contentBuff, self::HeaderInfoLength + $idx); - $ePtr = self::getLong($this->contentBuff, self::HeaderInfoLength + $idx + 4); - } else { - // read the vector index block - $buff = $this->read(self::HeaderInfoLength + $idx, 8); - if ($buff === null) { - throw new Exception("failed to read vector index at ${idx}"); - } - - $sPtr = self::getLong($buff, 0); - $ePtr = self::getLong($buff, 4); - } - - // printf("sPtr: %d, ePtr: %d\n", $sPtr, $ePtr); - - // binary search the segment index to get the region info - $dataLen = 0; - $dataPtr = null; - $l = 0; - $h = ($ePtr - $sPtr) / self::SegmentIndexSize; - while ($l <= $h) { - $m = ($l + $h) >> 1; - $p = $sPtr + $m * self::SegmentIndexSize; - - // read the segment index - $buff = $this->read($p, self::SegmentIndexSize); - if ($buff == null) { - throw new Exception("failed to read segment index at ${p}"); - } - - $sip = self::getLong($buff, 0); - if ($ip < $sip) { - $h = $m - 1; - } else { - $eip = self::getLong($buff, 4); - if ($ip > $eip) { - $l = $m + 1; - } else { - $dataLen = self::getShort($buff, 8); - $dataPtr = self::getLong($buff, 10); - break; - } - } - } - - // match nothing interception. - // @TODO: could this even be a case ? - // printf("dataLen: %d, dataPtr: %d\n", $dataLen, $dataPtr); - if ($dataPtr == null) { - return null; - } - - // load and return the region data - $buff = $this->read($dataPtr, $dataLen); - if ($buff == null) { - return null; - } - - return $buff; - } - - // read specified bytes from the specified index - private function read($offset, $len) { - // check the in-memory buffer first - if ($this->contentBuff != null) { - return substr($this->contentBuff, $offset, $len); - } - - // read from the file - $r = fseek($this->handle, $offset); - if ($r == -1) { - return null; - } - - $this->ioCount++; - $buff = fread($this->handle, $len); - if ($buff === false) { - return null; - } - - if (strlen($buff) != $len) { - return null; - } - - return $buff; - } - - // --- static util functions ---- - - // convert a string ip to long - public static function ip2long($ip) - { - $ip = ip2long($ip); - if ($ip === false) { - return null; - } - - // convert signed int to unsigned int if on 32 bit operating system - if ($ip < 0 && PHP_INT_SIZE == 4) { - $ip = sprintf("%u", $ip); - } - - return $ip; - } - - // read a 4bytes long from a byte buffer - public static function getLong($b, $idx) - { - $val = (ord($b[$idx])) | (ord($b[$idx+1]) << 8) - | (ord($b[$idx+2]) << 16) | (ord($b[$idx+3]) << 24); - - // convert signed int to unsigned int if on 32 bit operating system - if ($val < 0 && PHP_INT_SIZE == 4) { - $val = sprintf("%u", $val); - } - - return $val; - } - - // read a 2bytes short from a byte buffer - public static function getShort($b, $idx) - { - return ((ord($b[$idx])) | (ord($b[$idx+1]) << 8)); - } - - // load header info from a specified file handle - public static function loadHeader($handle) { - if (fseek($handle, 0) == -1) { - return null; - } - - $buff = fread($handle, self::HeaderInfoLength); - if ($buff === false) { - return null; - } - - // read bytes length checking - if (strlen($buff) != self::HeaderInfoLength) { - return null; - } - - // return the decoded header info - return array( - 'version' => self::getShort($buff, 0), - 'indexPolicy' => self::getShort($buff, 2), - 'createdAt' => self::getLong($buff, 4), - 'startIndexPtr' => self::getLong($buff, 8), - 'endIndexPtr' => self::getLong($buff, 12) - ); - } - - // load header info from the specified xdb file path - public static function loadHeaderFromFile($dbFile) { - $handle = fopen($dbFile, 'r'); - if ($handle === false) { - return null; - } - - $header = self::loadHeader($handle); - fclose($handle); - return $header; - } - - // load vector index from a file handle - public static function loadVectorIndex($handle) { - if (fseek($handle, self::HeaderInfoLength) == -1) { - return null; - } - - $rLen = self::VectorIndexRows * self::VectorIndexCols * self::SegmentIndexSize; - $buff = fread($handle, $rLen); - if ($buff === false) { - return null; - } - - if (strlen($buff) != $rLen) { - return null; - } - - return $buff; - } - - // load vector index from a specified xdb file path - public static function loadVectorIndexFromFile($dbFile) { - $handle = fopen($dbFile, 'r'); - if ($handle === false) { - return null; - } - - $vIndex = self::loadVectorIndex($handle); - fclose($handle); - return $vIndex; - } - - // load the xdb content from a file handle - public static function loadContent($handle) { - if (fseek($handle, 0, SEEK_END) == -1) { - return null; - } - - $size = ftell($handle); - if ($size === false) { - return null; - } - - // seek to the head for reading - if (fseek($handle, 0) == -1) { - return null; - } - - $buff = fread($handle, $size); - if ($buff === false) { - return null; - } - - // read length checking - if (strlen($buff) != $size) { - return null; - } - - return $buff; - } - - // load the xdb content from a file path - public static function loadContentFromFile($dbFile) { - $str = file_get_contents($dbFile, false); - if ($str === false) { - return null; - } else { - return $str; - } - } - - public static function now() { - return (microtime(true) * 1000); - } - -} diff --git a/binding/php/bench_test.php b/binding/php/bench_test.php index 2b9e30a..6ea7d4d 100644 --- a/binding/php/bench_test.php +++ b/binding/php/bench_test.php @@ -6,7 +6,11 @@ // @Author Lion // @Date 2022/06/22 -require dirname(__FILE__) . '/XdbSearcher.class.php'; +require dirname(__FILE__) . '/xdb/Searcher.class.php'; + +use \ip2region\xdb\Util; +use \ip2region\xdb\{IPv4, IPv6}; +use \ip2region\xdb\Searcher; function printHelp($argv) { printf("php %s [command options]\n", $argv[0]); @@ -60,39 +64,41 @@ if (strlen($dbFile) < 1 || strlen($srcFile) < 1) { } // printf("debug: dbFile: %s, cachePolicy: %s\n", $dbFile, $cachePolicy); +$version = IPv4::default(); + // create the xdb searcher by the cache-policy switch ( $cachePolicy ) { case 'file': try { - $searcher = XdbSearcher::newWithFileOnly($dbFile); + $searcher = Searcher::newWithFileOnly($version, $dbFile); } catch (Exception $e) { printf("failed to create searcher with '%s': %s\n", $dbFile, $e); return; } break; case 'vectorIndex': - $vIndex = XdbSearcher::loadVectorIndexFromFile($dbFile); + $vIndex = Util::loadVectorIndexFromFile($dbFile); if ($vIndex == null) { printf("failed to load vector index from '%s'\n", $dbFile); return; } try { - $searcher = XdbSearcher::newWithVectorIndex($dbFile, $vIndex); + $searcher = Searcher::newWithVectorIndex($version, $dbFile, $vIndex); } catch (Exception $e) { printf("failed to create vector index cached searcher with '%s': %s\n", $dbFile, $e); return; } break; case 'content': - $cBuff = XdbSearcher::loadContentFromFile($dbFile); + $cBuff = Util::loadContentFromFile($dbFile); if ($cBuff == null) { printf("failed to load xdb content from '%s'\n", $dbFile); return; } try { - $searcher = XdbSearcher::newWithBuffer($cBuff); + $searcher = Searcher::newWithBuffer($version, $cBuff); } catch (Exception $e) { printf("failed to create content cached searcher: %s", $e); return; @@ -113,7 +119,7 @@ if ($handle === false) { $count = 0; $costs = 0; -$sTime = XdbSearcher::now(); +$sTime = Util::now(); while (!feof($handle)) { $line = trim(fgets($handle, 1024)); if (strlen($line) < 1) { @@ -126,42 +132,44 @@ while (!feof($handle)) { return; } - $sip = XdbSearcher::ip2long($ps[0]); + $sip = Util::parseIP($ps[0]); if ($sip === null) { printf("invalid start ip `%s`\n", $ps[0]); return; } - $eip = XdbSearcher::ip2long($ps[1]); + $eip = Util::parseIP($ps[1]); if ($eip === null) { printf("invalid end ip `%s`\n", $ps[1]); return; } - if ($sip > $eip) { - printf("start ip(%s) should not be greater than end ip(%s)\n", $ps[0], $ps[1]); + if (Util::ipCompare($sip, $eip) > 0) { + printf( + "start ip(%s) should not be greater than end ip(%s)\n", + Util::ipToString($ps[0]), Util::ipToString($ps[1]) + ); return; } - $mip = ($sip + $eip) >> 1; - foreach ([$sip, ($sip + $mip) >> 1, $mip, ($mip + $eip) >> 1, $eip] as $ip) { + foreach ([$sip, $eip] as $ip) { try { - $cTime = XdbSearcher::now(); - $region = $searcher->search($ip); - $costs += XdbSearcher::now() - $cTime; + $cTime = Util::now(); + $region = $searcher->searchByBytes($ip); + $costs += Util::now() - $cTime; } catch (Exception $e) { - printf("failed to search ip `%s`\n", long2ip($ip)); + printf("failed to search ip `%s`: %s\n", Util::ipToString($ip), $e->getMessage()); return; } if ($region == null) { - printf("failed to search ip `%s`\n", long2ip($ip)); + printf("failed to search ip `%s`: empty region info\n", Util::ipToString($ip)); return; } // check the region info if ($region != $ps[2]) { - printf("failed search(%s) with (%s != %s)\n", long2ip($ip), $region, $ps[2]); + printf("failed search(%s) with (%s != %s)\n", Util::ipToString($ip), $region, $ps[2]); return; } @@ -173,4 +181,4 @@ while (!feof($handle)) { fclose($handle); $searcher->close(); printf("Bench finished, {cachePolicy: %s, total: %d, took: %ds, cost: %.3f ms/op}\n", - $cachePolicy, $count, (XdbSearcher::now() - $sTime)/1000, $count == 0 ? 0 : $costs/$count); + $cachePolicy, $count, (Util::now() - $sTime)/1000, $count == 0 ? 0 : $costs/$count); diff --git a/binding/php/search_test.php b/binding/php/search_test.php index 1a65036..cb62d60 100644 --- a/binding/php/search_test.php +++ b/binding/php/search_test.php @@ -6,7 +6,11 @@ // @Author Lion // @Date 2022/06/21 -require dirname(__FILE__) . '/XdbSearcher.class.php'; +require dirname(__FILE__) . '/xdb/Searcher.class.php'; + +use \ip2region\xdb\Util; +use \ip2region\xdb\{IPv4, IPv6}; +use \ip2region\xdb\Searcher; function printHelp($argv) { printf("php %s [command options]\n", $argv[0]); @@ -56,39 +60,41 @@ if (strlen($dbFile) < 1) { } // printf("debug: dbFile: %s, cachePolicy: %s\n", $dbFile, $cachePolicy); +$version = IPv4::default(); + // create the xdb searcher by the cache-policy switch ( $cachePolicy ) { case 'file': try { - $searcher = XdbSearcher::newWithFileOnly($dbFile); + $searcher = Searcher::newWithFileOnly($version, $dbFile); } catch (Exception $e) { printf("failed to create searcher with '%s': %s\n", $dbFile, $e); return; } break; case 'vectorIndex': - $vIndex = XdbSearcher::loadVectorIndexFromFile($dbFile); + $vIndex = Util::loadVectorIndexFromFile($dbFile); if ($vIndex == null) { printf("failed to load vector index from '%s'\n", $dbFile); return; } try { - $searcher = XdbSearcher::newWithVectorIndex($dbFile, $vIndex); + $searcher = Searcher::newWithVectorIndex($version, $dbFile, $vIndex); } catch (Exception $e) { printf("failed to create vector index cached searcher with '%s': %s\n", $dbFile, $e); return; } break; case 'content': - $cBuff = XdbSearcher::loadContentFromFile($dbFile); + $cBuff = Util::loadContentFromFile($dbFile); if ($cBuff == null) { printf("failed to load xdb content from '%s'\n", $dbFile); return; } try { - $searcher = XdbSearcher::newWithBuffer($cBuff); + $searcher = Searcher::newWithBuffer($version, $cBuff); } catch (Exception $e) { printf("failed to create content cached searcher: %s", $e); return; @@ -111,21 +117,20 @@ while ( true ) { break; } - if (XdbSearcher::ip2long($line) === null) { - echo "Error: invalid ip address\n"; - continue; - } - - $sTime = XdbSearcher::now(); + $cost = -1; try { + $sTime = Util::now(); $region = $searcher->search($line); + $cost = Util::now() - $sTime; } catch (Exception $e) { - printf("search call failed: %s\n", $e); + printf("search call failed: %s\n", $e->getMessage()); continue; } - printf("{region: %s, ioCount: %d, took: %.5f ms}\n", - $region, $searcher->getIOCount(), XdbSearcher::now() - $sTime); + printf( + "{region: %s, ioCount: %d, took: %.5f ms}\n", + $region, $searcher->getIOCount(), $cost + ); } // close the searcher at last diff --git a/binding/php/util_test.php b/binding/php/util_test.php deleted file mode 100644 index 072753e..0000000 --- a/binding/php/util_test.php +++ /dev/null @@ -1,57 +0,0 @@ - -// @Date 2022/06/22 - -require dirname(__FILE__) . '/XdbSearcher.class.php'; - -function testLoadHeader() { - $header = XdbSearcher::loadHeaderFromFile('../../data/ip2region.xdb'); - if ($header == null) { - printf("failed to load header from file\n"); - return; - } - - printf("header loaded: "); - print_r($header); -} - -function testLoadVectorIndex() { - $vIndex = XdbSearcher::loadVectorIndexFromFile('../../data/ip2region.xdb'); - if ($vIndex == null) { - printf("failed to load vector index from file\n"); - return; - } - - printf("vector index loaded: length=%d\n", strlen($vIndex)); -} - -function testLoadContent() { - $cBuff = XdbSearcher::loadContentFromFile('../../data/ip2region.xdb'); - if ($cBuff == null) { - printf("failed to load content from file\n"); - return; - } - - printf("content loaded, length=%d\n", strlen($cBuff)); -} - -printf("testing loadHeader ... \n"); -$now = XdbSearcher::now(); -testLoadHeader(); -printf("done, cost: %0.5f ms\n\n", XdbSearcher::now() - $now); - - -printf("testing loadVectorIndex ... \n"); -$now = XdbSearcher::now(); -testLoadVectorIndex(); -printf("done, cost: %0.5f ms\n\n", XdbSearcher::now() - $now); - - -printf("testing loadContent ... \n"); -$now = XdbSearcher::now(); -testLoadContent(); -printf("done, cost: %0.5f ms\n\n", XdbSearcher::now() - $now); diff --git a/binding/php/xdb/Searcher.class.php b/binding/php/xdb/Searcher.class.php new file mode 100644 index 0000000..fcc7033 --- /dev/null +++ b/binding/php/xdb/Searcher.class.php @@ -0,0 +1,486 @@ + +// @Date 2022/06/21 + +namespace ip2region\xdb; +use \Exception; + +// global constants +const Structure_20 = 2; +const Structure_30 = 3; +const IPv4VersionNo = 4; +const IPv6VersionNo = 6; +const HeaderInfoLength = 256; +const VectorIndexRows = 256; +const VectorIndexCols = 256; +const VectorIndexSize = 8; + + +// Util class +class Util { + // parse the specified IP address and return its bytes. + // returns: NULL for failed or the packed bytes + public static function parseIP($ipString) { + $flag = FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6; + if (!filter_var($ipString, FILTER_VALIDATE_IP, $flag)) { + return null; + } + + return inet_pton($ipString); + } + + // IP bytes to string + public static function ipToString($ipBytes) { + $l = strlen($ipBytes); + return ($l == 4 || $l == 16) ? inet_ntop($ipBytes) : ''; + } + + // compare two ip bytes (packed string return by parsedIP) + // returns: -1 if ip1 < ip2, 0 if ip1 == ip2 or 1 if ip1 > ip2 + public static function ipSubCompare($ip1, $buff, $offset) { + $r = substr_compare($ip1, $buff, $offset, strlen($ip1)); + if ($r < 0) { + return -1; + } else if ($r > 0) { + return 1; + } else { + return 0; + } + } + + // returns: -1 if ip1 < ip2, 0 if ip1 == ip2 or 1 if ip1 > ip2 + public static function ipCompare($ip1, $ip2) { + $r = strcmp($ip1, $ip2); + if ($r < 0) { + return -1; + } else if ($r > 0) { + return 1; + } else { + return 0; + } + } + + // decode a 4bytes long with Little endian byte order from a byte buffer + public static function le_getUint32($b, $idx) { + $val = (ord($b[$idx])) | (ord($b[$idx+1]) << 8) + | (ord($b[$idx+2]) << 16) | (ord($b[$idx+3]) << 24); + + // convert signed int to unsigned int if on 32 bit operating system + if ($val < 0 && PHP_INT_SIZE == 4) { + $val = sprintf("%u", $val); + } + + return $val; + } + + // read a 2bytes int with litten endian byte order from a byte buffer + public static function le_getUint16($b, $idx) { + return ((ord($b[$idx])) | (ord($b[$idx+1]) << 8)); + } + + // load header info from a specified file handle + public static function loadHeader($handle) { + if (fseek($handle, 0) == -1) { + return null; + } + + $buff = fread($handle, HeaderInfoLength); + if ($buff === false) { + return null; + } + + // read bytes length checking + if (strlen($buff) != HeaderInfoLength) { + return null; + } + + // return the decoded header info + return array( + 'version' => self::le_getUint16($buff, 0), + 'indexPolicy' => self::le_getUint16($buff, 2), + 'createdAt' => self::le_getUint32($buff, 4), + 'startIndexPtr' => self::le_getUint32($buff, 8), + 'endIndexPtr' => self::le_getUint32($buff, 12), + 'ipVersion' => self::le_getUint16($buff, 16), + 'runtimePtrBytes' => self::le_getUint16($buff, 18) + ); + } + + // load header info from the specified xdb file path + public static function loadHeaderFromFile($dbFile) { + $handle = fopen($dbFile, 'r'); + if ($handle === false) { + return null; + } + + $header = self::loadHeader($handle); + fclose($handle); + return $header; + } + + // load vector index from a file handle + public static function loadVectorIndex($handle) { + if (fseek($handle, HeaderInfoLength) == -1) { + return null; + } + + $rLen = VectorIndexRows * VectorIndexCols * VectorIndexSize; + $buff = fread($handle, $rLen); + if ($buff === false) { + return null; + } + + if (strlen($buff) != $rLen) { + return null; + } + + return $buff; + } + + // load vector index from a specified xdb file path + public static function loadVectorIndexFromFile($dbFile) { + $handle = fopen($dbFile, 'r'); + if ($handle === false) { + return null; + } + + $vIndex = self::loadVectorIndex($handle); + fclose($handle); + return $vIndex; + } + + // load the xdb content from a file handle + public static function loadContent($handle) { + if (fseek($handle, 0, SEEK_END) == -1) { + return null; + } + + $size = ftell($handle); + if ($size === false) { + return null; + } + + // seek to the head for reading + if (fseek($handle, 0) == -1) { + return null; + } + + $buff = fread($handle, $size); + if ($buff === false) { + return null; + } + + // read length checking + if (strlen($buff) != $size) { + return null; + } + + return $buff; + } + + // load the xdb content from a file path + public static function loadContentFromFile($dbFile) { + $str = file_get_contents($dbFile, false); + if ($str === false) { + return null; + } else { + return $str; + } + } + + public static function now() { + return (microtime(true) * 1000); + } +} + +// IPv4 version class +class IPv4 { + public $id; + public $name; + public $bytes; + public $segmentIndexSize; + + public static function default() { + // 14 = 4 + 4 + 2 + 4 + return new self(IPv4VersionNo, 'IPv4', 4, 14); + } + + public function __construct($id, $name, $bytes, $segmentIndexSize) { + $this->id = $id; + $this->name = $name; + $this->bytes = $bytes; + $this->segmentIndexSize = $segmentIndexSize; + } + + // compare the two ip bytes with the current version + public function ipSubCompare($ip1, $buff, $offset) { + // ip1: Little endian byte order encoded long from searcher. + // ip2: Little endian byte order read from xdb index. + // @Note: to compatible with the old Litten endian index encode implementation. + $ip2 = ( + (ord($buff[$offset ]) << 24) | + (ord($buff[$offset+1]) << 16) | + (ord($buff[$offset+2]) << 8) | ord($buff[$offset+3]) + ); + + $r = $ip1 - $ip2; + if ($r > 0) { + return 1; + } else if ($r < 0) { + return -1; + } else { + return 0; + } + } + + public function toString() { + return sprintf( + "{id:%d, name:%s, bytes:%d, segmentIndexSize:%d}", + $this->id, $this->name, $this->bytes, $this->segmentIndexSize + ); + } +} + +class IPv6 { + public $id; + public $name; + public $bytes; + public $segmentIndexSize; + + public static function default() { + // 38 = 16 + 16 + 2 + 4 + return new self(IPv6VersionNo, 'IPv6', 16, 38); + } + + public function __construct($id, $name, $bytes, $segmentIndexSize) { + $this->id = $id; + $this->name = $name; + $this->bytes = $bytes; + $this->segmentIndexSize = $segmentIndexSize; + } + + public function ipSubCompare($ip, $buff, $offset) { + return Util::ipSubCompare($ip, $buff, $offset); + } + + public function toString() { + return sprintf( + "{id:%d, name:%s, bytes:%d, segmentIndexSize:%d}", + $this->id, $this->name, $this->bytes, $this->segmentIndexSize + ); + } +} + +// Xdb searcher implementation +class Searcher { + // ip version + private $version; + + // xdb file handle + private $handle = null; + + private $ioCount = 0; + + // vector index in binary string. + // string decode will be faster than the map based Array. + private $vectorIndex = null; + + // xdb content buffer + private $contentBuff = null; + + // --- + // static function to create searcher + + /** + * @throws Exception + */ + public static function newWithFileOnly($version, $dbFile) { + return new self($version, $dbFile, null, null); + } + + /** + * @throws Exception + */ + public static function newWithVectorIndex($version, $dbFile, $vIndex) { + return new self($version, $dbFile, $vIndex, null); + } + + /** + * @throws Exception + */ + public static function newWithBuffer($version, $cBuff) { + return new self($version, null, null, $cBuff); + } + + // --- End of static creator + + /** + * initialize the xdb searcher + * @throws Exception + */ + function __construct($version, $dbFile, $vectorIndex=null, $cBuff=null) { + $this->version = $version; + // check the content buffer first + if ($cBuff != null) { + $this->vectorIndex = null; + $this->contentBuff = $cBuff; + } else { + // open the xdb binary file + $this->handle = fopen($dbFile, "r"); + if ($this->handle === false) { + throw new Exception("failed to open xdb file '%s'", $dbFile); + } + + $this->vectorIndex = $vectorIndex; + } + } + + public function close() { + if ($this->handle != null) { + fclose($this->handle); + } + } + + public function getIPVersion() { + return $this->version; + } + + public function getIOCount() { + return $this->ioCount; + } + + /** + * find the region info for the specified ip address. + * @Note: the ip address couldO ONLY be a human-readable IP address string, + * DO not use the packed binary string returned by #parseIP + * + * @throws Exception + */ + public function search($ip) { + $ipBytes = Util::parseIP($ip); + if ($ipBytes == null) { + throw new Exception("invalid ip address `{$ip}`"); + } + + return $this->searchByBytes($ipBytes); + } + + /** + * find the region info for the specified binary ip bytes returned by #parseIP. + * + * @throws Exception + */ + public function searchByBytes($ipBytes) { + // ip version check + if (strlen($ipBytes) != $this->version->bytes) { + throw new Exception("invalid ip address ({$this->version->name} expected)"); + } + + // reset the global counter + $this->ioCount = 0; + + // locate the segment index block based on the vector index + $il0 = ord($ipBytes[0]) & 0xFF; + $il1 = ord($ipBytes[1]) & 0xFF; + $idx = $il0 * VectorIndexCols * VectorIndexSize + $il1 * VectorIndexSize; + if ($this->vectorIndex != null) { + $sPtr = Util::le_getUint32($this->vectorIndex, $idx); + $ePtr = Util::le_getUint32($this->vectorIndex, $idx + 4); + } else if ($this->contentBuff != null) { + $sPtr = Util::le_getUint32($this->contentBuff, HeaderInfoLength + $idx); + $ePtr = Util::le_getUint32($this->contentBuff, HeaderInfoLength + $idx + 4); + } else { + // read the vector index block + $buff = $this->read(HeaderInfoLength + $idx, 8); + if ($buff === null) { + throw new Exception("failed to read vector index at ${idx}"); + } + + $sPtr = Util::le_getUint32($buff, 0); + $ePtr = Util::le_getUint32($buff, 4); + } + + // printf("sPtr: %d, ePtr: %d\n", $sPtr, $ePtr); + [$bytes, $dBytes] = [strlen($ipBytes), strlen($ipBytes) << 1]; + if ($bytes == 4) { + // encode the IPv4 bytes to an long with Litten endian byte order + // to avoid the repeated calcs in the binary search loop. + $ipBytes = ( + (ord($ipBytes[3]) << 24) | + (ord($ipBytes[2]) << 24) | + (ord($ipBytes[1]) << 24) | (ord($ipBytes[0])) + ); + } + + // binary search the segment index to get the region info + $idxSize = $this->version->segmentIndexSize; + [$dataLen, $dataPtr] = [0, null]; + [$l, $h] = [0, ($ePtr - $sPtr) / $idxSize]; + while ($l <= $h) { + $m = ($l + $h) >> 1; + $p = $sPtr + $m * $idxSize; + + // read the segment index + $buff = $this->read($p, $idxSize); + if ($buff == null) { + throw new Exception("failed to read segment index with ptr={$p}"); + } + + if ($this->version->ipSubCompare($ipBytes, $buff, 0) < 0) { + $h = $m - 1; + } else if ($this->version->ipSubCompare($ipBytes, $buff, $bytes) > 0) { + $l = $m + 1; + } else { + $dataLen = Util::le_getUint16($buff, $dBytes); + $dataPtr = Util::le_getUint32($buff, $dBytes + 2); + break; + } + } + + // match nothing interception. + // @TODO: could this even be a case ? + // printf("dataLen: %d, dataPtr: %d\n", $dataLen, $dataPtr); + if ($dataPtr == null) { + return null; + } + + // load and return the region data + $buff = $this->read($dataPtr, $dataLen); + if ($buff == null) { + return null; + } + + return $buff; + } + + // read specified bytes from the specified index + private function read($offset, $len) { + // check the in-memory buffer first + if ($this->contentBuff != null) { + return substr($this->contentBuff, $offset, $len); + } + + // read from the file + $r = fseek($this->handle, $offset); + if ($r == -1) { + return null; + } + + $this->ioCount++; + $buff = fread($this->handle, $len); + if ($buff === false) { + return null; + } + + if (strlen($buff) != $len) { + return null; + } + + return $buff; + } + +} \ No newline at end of file diff --git a/binding/php/xdb/util_test.php b/binding/php/xdb/util_test.php new file mode 100644 index 0000000..d5baf9c --- /dev/null +++ b/binding/php/xdb/util_test.php @@ -0,0 +1,118 @@ + +// @Date 2022/06/22 + +require dirname(__FILE__) . '/Searcher.class.php'; +use \ip2region\xdb\Util; +use \ip2region\xdb\Searcher; +use \ip2region\xdb\IPv4; +use \ip2region\xdb\IPv6; + +// check and get the function to run +if($argc < 2) { + printf("please specified the function name\n"); + return; +} else { + $func_name = trim($argv[1]); +} + + +function testLoadHeader() { + $header = Util::loadHeaderFromFile('../../data/ip2region_v4.xdb'); + if ($header == null) { + printf("failed to load header from file\n"); + return; + } + + printf("header loaded: "); + print_r($header); +} + +function testLoadVectorIndex() { + $vIndex = Util::loadVectorIndexFromFile('../../data/ip2region_v4.xdb'); + if ($vIndex == null) { + printf("failed to load vector index from file\n"); + return; + } + + printf("vector index loaded: length=%d\n", strlen($vIndex)); +} + +function testLoadContent() { + $cBuff = Util::loadContentFromFile('../../data/ip2region_v4.xdb'); + if ($cBuff == null) { + printf("failed to load content from file\n"); + return; + } + + printf("content loaded, length=%d\n", strlen($cBuff)); +} + +function testParseIP() { + $ips = [ + // IPv4 + "1.0.0.1", + "192.168.1.100", + "121.35.184.170", + + "xx.xx.1.100", + + // IPv6 + "3000::", + "240e:87c:71a:639a:3dff:ffff:ffff:ffff", + "240e:87c:71a:c877:900::" + ]; + + foreach ($ips as $ip) { + $bytes = Util::parseIP($ip); + if ($bytes == NULL) { + printf("invalid ip address: `%s`\n", $ip); + continue; + } + + // for ($i = 0; $i < strlen($bytes); $i++) { + // printf("%d: []: %s, ord: %d\n", $i, $bytes[$i], ord($bytes[$i])); + // } + + printf("bytes: %s (%s), address: %s\n", bin2hex($bytes), gettype($bytes), Util::ipToString($bytes)); + } +} + +function testIPCompare() { + $ipPairs = [ + ["1.0.0.0", "1.0.0.1"], + ["192.168.1.101", "192.168.1.90"], + ["219.133.111.87", "114.114.114.114"], + ["2000::", "2000:ffff:ffff:ffff:ffff:ffff:ffff:ffff"], + ["2001:4:112::", "2001:4:112:ffff:ffff:ffff:ffff:ffff"], + ["ffff::", "2001:4:ffff:ffff:ffff:ffff:ffff:ffff"] + ]; + + foreach ($ipPairs as $ips) { + $ip1 = Util::parseIP($ips[0]); + $ip2 = Util::parseIP($ips[1]); + printf("ipCompare(%s, %s): %d\n", Util::ipToString($ip1), Util::ipToString($ip2), Util::ipSubCompare($ip1, $ip2, 0)); + } +} + +function testAttributes() { + printf("IPv4VersioNo: %d\n", \ip2region\xdb\IPv4VersionNo); + printf("IPv6VersioNo: %d\n", \ip2region\xdb\IPv6VersionNo); + printf("IPv4 Object: %s\n", IPv4::default()->toString()); + printf("IPv6 Object: %s\n", IPv6::default()->toString()); +} + + +if (!function_exists($func_name)) { + printf("function {$func_name} not found\n"); +} else { + printf("calling {$func_name} ... \n"); + $now = Util::now(); + $func_name(); + $cost = Util::now() - $now; + printf("done, cost: %0.5f ms\n", $cost); +} \ No newline at end of file diff --git a/binding/php5_ext/ReadMe.md b/binding/php5_ext/ReadMe.md deleted file mode 100644 index 587e368..0000000 --- a/binding/php5_ext/ReadMe.md +++ /dev/null @@ -1,7 +0,0 @@ -# ip2region php5 c 扩展查询客户端实现 - -# 使用方式 - -# 查询测试 - -# bench 测试 diff --git a/binding/php7_ext/ReadMe.md b/binding/php7_ext/ReadMe.md deleted file mode 100644 index 283d26c..0000000 --- a/binding/php7_ext/ReadMe.md +++ /dev/null @@ -1,7 +0,0 @@ -# ip2region php7 c 扩展查询客户端实现 - -# 使用方式 - -# 查询测试 - -# bench 测试