You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

444 lines
11 KiB
Lua

--[[
Ip2region lua binding
@author chenxin<chenxin619315@gmail.com>
@date 2018/10/02
]]--
require("bit32");
local INDEX_BLOCK_LENGTH = 12;
local TOTAL_HEADER_LENGTH = 8192;
local _M = {
dbFile = "",
dbFileHandler = "",
dbBinStr = "",
HeaderSip = "",
HeaderPtr = "",
headerLen = 0,
firstIndexPtr = 0,
lastIndexPtr = 0,
totalBlocks = 0
};
_G["Ip2region"] = _M;
-- set the __index to itself
_M.__index = _M;
-- set the print meta-method
_M.__tostring = function(table)
local t = {
"dbFile=" .. table.dbFile,
-- "dbFileHandler=" .. table.dbFileHandler,
"headerLen=" .. table.headerLen,
"firstIndexPtr" .. table.firstIndexPtr,
"lastIndexPtr" .. table.lastIndexPtr,
"totalBlocks" .. table.totalBlocks
};
return table.concat(t, ",");
end
--[[
construct method
@param obj
@return Ip2region object
--]]
function _M.new(dbFile)
obj = {};
setmetatable(obj, _M);
obj.dbFile = dbFile;
return obj;
end
--[[
internal function to get a integer from a binary string
@param dbBinStr
@param idx
@return Integer
]]--
function getLong(bs, idx)
local a1 = string.byte(string.sub(bs, idx, idx));
local a2 = bit32.lshift(string.byte(string.sub(bs, idx+1, idx+1)), 8);
local a3 = bit32.lshift(string.byte(string.sub(bs, idx+2, idx+2)), 16);
local a4 = bit32.lshift(string.byte(string.sub(bs, idx+3, idx+3)), 24);
local val = bit32.bor(a1, a2);
val = bit32.bor(val, a3);
val = bit32.bor(val, a4);
return val;
end
--[[
internal function to convert the string ip to a long value
@param ip
@return Integer
]]--
function _M.ip2long(self, ip)
-- dynamic arguments checking
-- to support object.ip2long and object:ip2long access
if ( type(self) == "string") then
ip = self;
end
local ini = 1;
local iip = 0;
local off = 24;
while true do
local pos = string.find(ip, '.', ini, true);
if ( not pos ) then
break;
end
local sub = string.sub(ip, ini, pos - 1);
if ( string.len(sub) < 1 ) then
return nil;
end
iip = bit32.bor(iip, bit32.lshift(tonumber(sub), off));
ini = pos + 1;
off = off - 8;
end
-- check if it is a valid ip address
if ( off ~= 0 or ini > string.len(ip) ) then
return nil;
end
local sub = string.sub(ip, ini);
if ( string.len(sub) < 1 ) then
return nil;
end
return bit32.bor(iip, bit32.lshift(tonumber(sub), off));
end
--[[
internal function to get the whole content of a file
@param file
@return String
]]--
function get_file_contents(file)
local fi = io.input(file);
if ( not fi ) then
return nil;
end
local str = io.read("*a");
io.close();
return str;
end
--[[
set the current db file path
@param dbFile
]]--
function _M:setDbFile(dbFile)
self.dbFile = dbFile;
end
--[[
all the db binary string will be loaded into memory
then search the memory only and this will a lot faster than disk base search
@Note: invoke it once before put it to public invoke could make it thread safe
@param ip
@return table or nil for failed
]]--
function _M:memorySearch(ip)
-- string ip conversion
if ( type(ip) == "string" ) then
ip = self:ip2long(ip);
if ( ip == nil ) then
return nil;
end
end;
-- check and load the binary string for the first time
if ( self.dbBinStr == "" ) then
self.dbBinStr = get_file_contents(self.dbFile);
if ( not self.dbBinStr ) then
return nil;
end
self.firstIndexPtr = getLong(self.dbBinStr, 1);
self.lastIndexPtr = getLong(self.dbBinStr, 5);
self.totalBlocks = (self.lastIndexPtr - self.firstIndexPtr)/INDEX_BLOCK_LENGTH + 1;
end
-- binary search to define the data
local l = 0;
local h = self.totalBlocks;
local dataPtr = 0;
while ( l <= h ) do
local m = math.floor((l + h) / 2);
local p = self.firstIndexPtr + m * INDEX_BLOCK_LENGTH;
local sip = getLong(self.dbBinStr, p + 1);
if ( ip < sip ) then
h = m - 1;
else
local eip = getLong(self.dbBinStr, p + 5); -- 4 + 1
if ( ip > eip ) then
l = m + 1;
else
dataPtr = getLong(self.dbBinStr, p + 9); -- 8 + 1
break;
end
end
end
-- not matched just stop it here
if ( dataPtr == 0 ) then return nil end
-- get the data
local dataLen = bit32.band(bit32.rshift(dataPtr, 24), 0xFF);
dataPtr = bit32.band(dataPtr, 0x00FFFFFF);
local dptr = dataPtr + 5; -- 4 + 1
return {
city_id = getLong(self.dbBinStr, dataPtr),
region = string.sub(self.dbBinStr, dptr, dptr + dataLen - 5)
};
end
--[[
get the data block through the specified ip address
or long ip numeric with binary search algorithm
@param ip
@return table or nil for failed
]]--
function _M:binarySearch(ip)
-- check and conver the ip address
if ( type(ip) == "string" ) then
ip = self:ip2long(ip);
if ( ip == nil ) then
return nil;
end
end
if ( self.totalBlocks == 0 ) then
-- check and open the original db file
self.dbFileHandler = io.open(self.dbFile, "r");
if ( not self.dbFileHandler ) then
return nil;
end
self.dbFileHandler:seek("set", 0);
local superBlock = self.dbFileHandler:read(8);
self.firstIndexPtr = getLong(superBlock, 1); -- 0 + 1
self.lastIndexPtr = getLong(superBlock, 5); -- 4 + 1
self.totalBlocks = (self.lastIndexPtr-self.firstIndexPtr)/INDEX_BLOCK_LENGTH + 1;
end
-- binary search to define the data
local l = 0;
local h = self.totalBlocks;
local dataPtr = 0;
while ( l <= h ) do
local m = math.floor((l + h) / 2);
local p = m * INDEX_BLOCK_LENGTH;
self.dbFileHandler:seek("set", self.firstIndexPtr + p);
local buffer = self.dbFileHandler:read(INDEX_BLOCK_LENGTH);
local sip = getLong(buffer, 1); -- 0 + 1
if ( ip < sip ) then
h = m - 1;
else
local eip = getLong(buffer, 5); -- 4 + 1
if ( ip > eip ) then
l = m + 1;
else
dataPtr = getLong(buffer, 9); -- 8 + 1
break;
end
end
end
-- not matched just stop it here
if ( dataPtr == 0 ) then return nil; end
-- get the data
local dataLen = bit32.band(bit32.rshift(dataPtr, 24), 0xFF);
dataPtr = bit32.band(dataPtr, 0x00FFFFFF);
self.dbFileHandler:seek("set", dataPtr);
local data = self.dbFileHandler:read(dataLen);
return {
city_id = getLong(data, 1), -- 0 + 1
region = string.sub(data, 5) -- 4 + 1
};
end
--[[
get the data block associated with the specified ip with b-tree search algorithm
@param ip
@return table or nil for failed
]]--
function _M:btreeSearch(ip)
-- string ip to integer conversion
if ( type(ip) == "string" ) then
ip = self:ip2long(ip);
if ( ip == nil ) then
return nil;
end
end
-- check and load the header
if ( self.headerLen == 0 ) then
-- check and open the original db file
self.dbFileHandler = io.open(self.dbFile, 'r');
if ( not self.dbFileHandler ) then
return nil;
end
self.dbFileHandler:seek("set", 8);
local buffer = self.dbFileHandler:read(TOTAL_HEADER_LENGTH);
-- fill the header
local i = 0;
local idx = 0;
self.HeaderSip = {};
self.HeaderPtr = {};
for i=0, TOTAL_HEADER_LENGTH, 8 do
local startIp = getLong(buffer, i + 1); -- 0 + 1
local dataPtr = getLong(buffer, i + 5); -- 4 + 1
if ( dataPtr == 0 ) then
break;
end
table.insert(self.HeaderSip, startIp);
table.insert(self.HeaderPtr, dataPtr);
idx = idx + 1;
end
self.headerLen = idx;
end
-- 1. define the index block with the binary search
local l = 0;
local h = self.headerLen;
local sptr = 0;
local eptr = 0;
while ( l <= h ) do
local m = math.floor((l + h) / 2);
-- perfetc matched, just return it
if ( ip == self.HeaderSip[m] ) then
if ( m > 0 ) then
sptr = self.HeaderPtr[m-1];
eptr = self.HeaderPtr[m ];
else
sptr = self.HeaderPtr[m ];
eptr = self.HeaderPtr[m+1];
end
break;
end
-- less then the middle value
if ( ip < self.HeaderSip[m] ) then
if ( m == 0 ) then
sptr = self.HeaderPtr[m ];
eptr = self.HeaderPtr[m+1];
break;
elseif ( ip > self.HeaderSip[m-1] ) then
sptr = self.HeaderPtr[m-1];
eptr = self.HeaderPtr[m ];
break;
end
h = m - 1;
else
if ( m == self.headerLen - 1 ) then
sptr = self.HeaderPtr[m-1];
eptr = self.HeaderPtr[m ];
break;
elseif ( ip <= self.HeaderSip[m+1] ) then
sptr = self.HeaderPtr[m ];
eptr = self.HeaderPtr[m+1];
break;
end
l = m + 1;
end
end
-- match nothing just stop it
if ( sptr == 0 ) then return nil; end
-- 2. search the index blocks to define the data
self.dbFileHandler:seek("set", sptr);
local blockLen = eptr - sptr;
local index = self.dbFileHandler:read(blockLen + INDEX_BLOCK_LENGTH);
local dataPtr = 0;
l = 0;
h = blockLen / INDEX_BLOCK_LENGTH;
while ( l <= h ) do
local m = math.floor((l + h) / 2);
local p = m * INDEX_BLOCK_LENGTH;
local sip = getLong(index, p + 1); -- 0 + 1
if ( ip < sip ) then
h = m - 1;
else
local eip = getLong(index, p + 5); -- 4 + 1
if ( ip > eip ) then
l = m + 1;
else
dataPtr = getLong(index, p + 9); -- 8 + 1
break;
end
end
end
-- not matched
if ( dataPtr == 0 ) then return nil; end
-- 3. get the data
local dataLen = bit32.band(bit32.rshift(dataPtr, 24), 0xFF);
dataPtr = bit32.band(dataPtr, 0x00FFFFFF);
self.dbFileHandler:seek("set", dataPtr);
local data = self.dbFileHandler:read(dataLen);
return {
city_id = getLong(data, 1), -- 0 + 1
region = string.sub(data, 5) -- 4 + 1
};
end
--[[
close the object and do the basic gc
]]--
function _M.close(self)
if ( self.dbFileHandler ~= "" ) then
self.dbFileHandler:close();
end
if ( self.dbBinStr ~= "" ) then
self.dbBinStr = nil;
end
end
return _M;