fh = @fopen( $path, 'rb' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- streams a 7 MB binary database a few bytes at a time; WP_Filesystem has no seek/partial read. if ( ! $this->fh ) { throw new RuntimeException( 'Cannot open database' ); } $this->file_size = filesize( $path ); $read_len = min( $this->file_size, 128 * 1024 ); fseek( $this->fh, $this->file_size - $read_len ); $tail = fread( $this->fh, $read_len ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread -- streams a 7 MB binary database a few bytes at a time; WP_Filesystem has no seek/partial read. $pos = strrpos( $tail, self::METADATA_MARKER ); if ( false === $pos ) { throw new RuntimeException( 'Invalid database' ); } $meta_start = $this->file_size - $read_len + $pos + strlen( self::METADATA_MARKER ); $this->data_offset = $meta_start; list( $meta ) = $this->decode( $meta_start ); $this->data_offset = 0; $this->node_count = (int) $meta['node_count']; $this->record_size = (int) $meta['record_size']; $this->ip_version = (int) $meta['ip_version']; $this->node_byte_size = $this->record_size * 2 / 8; $this->search_tree_size = $this->node_count * $this->node_byte_size; $this->data_offset = $this->search_tree_size + 16; if ( ! in_array( $this->record_size, array( 24, 28, 32 ), true ) ) { throw new RuntimeException( 'Unsupported record size' ); } } public function __destruct() { if ( $this->fh ) { fclose( $this->fh ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- streams a 7 MB binary database a few bytes at a time; WP_Filesystem has no seek/partial read. } } /** * @return mixed|null Record for the IP, or null if not found. */ public function get( $ip ) { $packed = @inet_pton( $ip ); if ( false === $packed ) { return null; } $bit_count = strlen( $packed ) * 8; if ( 4 === strlen( $packed ) && 6 === $this->ip_version ) { $node = $this->ipv4_start_node(); } elseif ( 16 === strlen( $packed ) && 4 === $this->ip_version ) { return null; } else { $node = 0; } for ( $i = 0; $i < $bit_count && $node < $this->node_count; $i++ ) { $bit = 1 & ( ord( $packed[ $i >> 3 ] ) >> ( 7 - ( $i % 8 ) ) ); $node = $this->read_node( $node, $bit ); } if ( $node === $this->node_count ) { return null; } if ( $node > $this->node_count ) { $offset = $node - $this->node_count - 16 + $this->search_tree_size + 16; list( $record ) = $this->decode( $offset ); return $record; } return null; } private function ipv4_start_node() { if ( null === $this->ipv4_start ) { $node = 0; for ( $i = 0; $i < 96 && $node < $this->node_count; $i++ ) { $node = $this->read_node( $node, 0 ); } $this->ipv4_start = $node; } return $this->ipv4_start; } private function read( $offset, $len ) { if ( 0 === $len ) { return ''; } fseek( $this->fh, $offset ); return fread( $this->fh, $len ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread -- streams a 7 MB binary database a few bytes at a time; WP_Filesystem has no seek/partial read. } private function read_node( $node, $index ) { $base = $node * $this->node_byte_size; switch ( $this->record_size ) { case 24: $b = $this->read( $base + $index * 3, 3 ); return unpack( 'N', "\0" . $b )[1]; case 28: $b = $this->read( $base, 7 ); if ( 0 === $index ) { return ( ( ord( $b[3] ) & 0xF0 ) << 20 ) | unpack( 'N', "\0" . substr( $b, 0, 3 ) )[1]; } return ( ( ord( $b[3] ) & 0x0F ) << 24 ) | unpack( 'N', "\0" . substr( $b, 4, 3 ) )[1]; default: $b = $this->read( $base + $index * 4, 4 ); return unpack( 'N', $b )[1]; } } /** * Decode a data-section value. Offsets are absolute file offsets. * * @return array [ value, next_offset ] */ private function decode( $offset ) { $ctrl = ord( $this->read( $offset, 1 ) ); $offset++; $type = $ctrl >> 5; if ( 1 === $type ) { // Pointer. $size = ( $ctrl >> 3 ) & 0x3; $vvv = $ctrl & 0x7; $buf = $this->read( $offset, $size + 1 ); $offset += $size + 1; switch ( $size ) { case 0: $ptr = ( $vvv << 8 ) | ord( $buf[0] ); break; case 1: $ptr = ( ( $vvv << 16 ) | unpack( 'n', $buf )[1] ) + 2048; break; case 2: $ptr = ( ( $vvv << 24 ) | unpack( 'N', "\0" . $buf )[1] ) + 526336; break; default: $ptr = unpack( 'N', $buf )[1]; } list( $value ) = $this->decode( $this->data_offset + $ptr ); return array( $value, $offset ); } if ( 0 === $type ) { // Extended type. $type = 7 + ord( $this->read( $offset, 1 ) ); $offset++; } $size = $ctrl & 0x1F; if ( $size >= 29 ) { $n = $size - 28; $bytes = $this->read( $offset, $n ); $offset += $n; if ( 29 === $size ) { $size = 29 + ord( $bytes ); } elseif ( 30 === $size ) { $size = 285 + unpack( 'n', $bytes )[1]; } else { $size = 65821 + unpack( 'N', "\0" . $bytes )[1]; } } switch ( $type ) { case 2: // UTF-8 string. return array( $this->read( $offset, $size ), $offset + $size ); case 3: // Double. return array( unpack( 'E', $this->read( $offset, 8 ) )[1], $offset + 8 ); case 4: // Bytes. return array( $this->read( $offset, $size ), $offset + $size ); case 5: // uint16. case 6: // uint32. case 9: // uint64. case 10: // uint128. case 8: // int32. $v = 0; $b = $this->read( $offset, $size ); for ( $i = 0; $i < $size; $i++ ) { $v = ( $v * 256 ) + ord( $b[ $i ] ); } if ( 8 === $type && 4 === $size && $v >= 2147483648 ) { $v -= 4294967296; } return array( $v, $offset + $size ); case 7: // Map. $map = array(); for ( $i = 0; $i < $size; $i++ ) { list( $k, $offset ) = $this->decode( $offset ); list( $v, $offset ) = $this->decode( $offset ); $map[ $k ] = $v; } return array( $map, $offset ); case 11: // Array. $arr = array(); for ( $i = 0; $i < $size; $i++ ) { list( $v, $offset ) = $this->decode( $offset ); $arr[] = $v; } return array( $arr, $offset ); case 14: // Boolean. return array( 0 !== $size, $offset ); case 15: // Float. return array( unpack( 'G', $this->read( $offset, 4 ) )[1], $offset + 4 ); default: return array( null, $offset + $size ); } } }