├── .gitignore ├── .luacheckrc ├── .gitmodules ├── .luacov ├── NEWS ├── spec └── test-data_spec.lua ├── mmdblua-scm-0.rockspec ├── example.lua ├── README.md ├── LICENSE.md └── mmdb └── init.lua /.gitignore: -------------------------------------------------------------------------------- 1 | GeoLite2-City.mmdb 2 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | std = "min" 2 | files["spec"] = { 3 | std = "+busted"; 4 | } 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "spec/MaxMind-DB"] 2 | path = spec/MaxMind-DB 3 | url = https://github.com/maxmind/MaxMind-DB 4 | -------------------------------------------------------------------------------- /.luacov: -------------------------------------------------------------------------------- 1 | return { 2 | statsfile = "luacov.stats.out"; 3 | reportfile = "luacov.report.out"; 4 | deletestats = true; 5 | include = { 6 | "/mmdb/[^/]+$"; 7 | }; 8 | exclude = { 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | UNRELEASED 2 | 3 | - Deprecate 'open' constructor (use 'read' instead) 4 | - Fix bug when reading boolean values (@Prasaana-sr) 5 | 6 | 7 | 0.1 - 2016-12-22 8 | 9 | - First release 10 | -------------------------------------------------------------------------------- /spec/test-data_spec.lua: -------------------------------------------------------------------------------- 1 | -- TODO: a real 'ls', this is a quick hack 2 | local function ls(dir) 3 | return assert(io.popen("ls -1 " .. dir)):lines() 4 | end 5 | 6 | describe("mmdb", function() 7 | local mmdb = require "mmdb" 8 | for file in ls("spec/MaxMind-DB/test-data/*.mmdb") do 9 | it("can open MaxMind test data file " .. file, function() 10 | mmdb.open(file) 11 | end) 12 | end 13 | end) 14 | -------------------------------------------------------------------------------- /mmdblua-scm-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "mmdblua" 2 | version = "scm-0" 3 | 4 | source = { 5 | url = "git://github.com/daurnimator/mmdblua.git"; 6 | } 7 | 8 | description = { 9 | summary = "Library for reading MaxMind's Geolocation database format."; 10 | license = "MIT"; 11 | } 12 | 13 | dependencies = { 14 | "lua >= 5.1"; 15 | "compat53 >= 0.3"; -- Only if lua < 5.3 16 | } 17 | 18 | build = { 19 | type = "builtin"; 20 | modules = { 21 | ["mmdb"] = "mmdb/init.lua"; 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /example.lua: -------------------------------------------------------------------------------- 1 | -- Simple pretty printer 2 | local function pp(ob, indent, stream) 3 | stream = stream or io.stderr 4 | if type(ob) == "table" then 5 | indent = indent or 0 6 | stream:write("{\n") 7 | do 8 | for k,v in pairs(ob) do 9 | stream:write(("\t"):rep(indent+1), k, " = ") 10 | pp(v, indent+1) 11 | end 12 | end 13 | assert(stream:write(("\t"):rep(indent), "}\n")) 14 | elseif type(ob) == "string" then 15 | assert(stream:write(string.format("%q\n", ob))) 16 | else 17 | assert(stream:write(tostring(ob), "\n")) 18 | end 19 | end 20 | 21 | -- Download from http://dev.maxmind.com/geoip/geoip2/geolite2/ 22 | local mmdb = require "mmdb" 23 | local geodb = assert(mmdb.read("GeoLite2-City.mmdb")) 24 | 25 | if arg[1] then 26 | pp(geodb:search_ipv4(arg[1])) 27 | else 28 | pp(geodb:search_ipv4 "213.215.63.11") -- french hotel near lua workshop 2013 29 | pp(geodb:search_ipv6 "2607:f8b0:4004:801::100e") -- google.com for me one day 30 | end 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mmdblua 2 | 3 | A Lua library for reading [MaxMind's Geolocation database format](https://maxmind.github.io/MaxMind-DB/). 4 | 5 | This project had it's initial development sprint done in a hotel room during the [Lua Workshop 2013](https://www.lua.org/wshop13.html). 6 | 7 | 8 | # Installation 9 | 10 | mmdblua is available from [luarocks](https://luarocks.org/). 11 | 12 | $ luarocks install mmdblua 13 | 14 | 15 | ## Dependencies 16 | 17 | If using lua < 5.3 you will need 18 | 19 | - [compat-5.3](https://github.com/keplerproject/lua-compat-5.3) >= 0.3 20 | 21 | 22 | # Development 23 | 24 | ## Getting started 25 | 26 | - Clone the repo: 27 | ``` 28 | $ git clone --recursive https://github.com/daurnimator/mmdblua.git 29 | $ cd mmdblua 30 | ``` 31 | *Note that mmdblua has a git submodule for test data.* 32 | 33 | - Lint the code (check for common programming errors) 34 | ``` 35 | $ luacheck . 36 | ``` 37 | 38 | - Run tests 39 | ``` 40 | $ busted 41 | ``` 42 | 43 | - Install your local copy: 44 | ``` 45 | $ luarocks make mmdblua-scm-0.rockspec 46 | ``` 47 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2016 Daurnimator 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /mmdb/init.lua: -------------------------------------------------------------------------------- 1 | -- This implements a lua parser of http://maxmind.github.io/MaxMind-DB/ 2 | 3 | local has_bit, bit = pcall(require, "bit") 4 | local has_ffi, ffi = pcall(require, "ffi") 5 | local sunpack = string.unpack or require "compat53.string".unpack -- luacheck: ignore 143 6 | 7 | local mmdb_separator = "\171\205\239MaxMind.com" 8 | 9 | local geodb_methods = {} 10 | local geodb_mt = { 11 | __name = "mmdblua-database"; 12 | __index = geodb_methods; 13 | } 14 | local data_types = {} 15 | local getters = {} 16 | 17 | local function new(contents) 18 | local start_metadata do 19 | -- Find data section seperator; at most it's 128kb from the end 20 | local init = math.max(1, #contents-(128*1024)) 21 | while true do 22 | local s, e = contents:find(mmdb_separator, start_metadata or init, true) 23 | if s == nil then break end 24 | start_metadata = e + 1 25 | end 26 | if start_metadata == nil then 27 | return nil, "Invalid MaxMind Database" 28 | end 29 | end 30 | 31 | local self = setmetatable({ 32 | contents = contents; 33 | start_metadata = start_metadata; 34 | data = nil; 35 | left = nil; 36 | right = nil; 37 | ipv4_start = 0; 38 | }, geodb_mt) 39 | 40 | local _, data = self:read_data(start_metadata, 0) 41 | self.data = data 42 | 43 | local getter = getters[data.record_size] 44 | if getter == nil then 45 | return nil, "Unsupported record size: " .. data.record_size 46 | end 47 | self.left, self.right, self.record_length = getter.left, getter.right, getter.record_length 48 | 49 | self.start_data = self.record_length * self.data.node_count + 16 + 1 50 | 51 | if self.data.ip_version == 6 then 52 | self.ipv4_start = self:ipv6_find_ipv4_start() 53 | end 54 | 55 | return self 56 | end 57 | 58 | local function read(filename) 59 | local fd, err, errno = io.open(filename, "rb") 60 | if not fd then 61 | return nil, err, errno 62 | end 63 | local contents, err2, errno2 = fd:read("*a") 64 | fd:close() 65 | if not contents then 66 | return nil, err2, errno2 67 | end 68 | return new(contents) 69 | end 70 | 71 | -- Deprecated: 72 | local function open(filename) 73 | return assert(read(filename)) 74 | end 75 | 76 | function geodb_methods:read_data(base, offset) 77 | local control_byte = self.contents:byte(base + offset) 78 | offset = offset + 1 79 | 80 | -- The first three bits of the control byte tell you what type the field is. 81 | local data_type = math.floor(control_byte / 32) 82 | -- If these bits are all 0, then this is an "extended" type, 83 | -- which means that the next byte contains the actual type. 84 | if data_type == 0 then 85 | data_type = self.contents:byte(base + offset) + 7 86 | offset = offset + 1 87 | end 88 | 89 | local func = data_types[data_type] 90 | if func == nil then 91 | error("Unknown data section: " .. data_type) 92 | end 93 | 94 | -- The next five bits in the control byte tell you how long the data 95 | -- field's payload is, except for maps and pointers. 96 | local data_size = control_byte % 32 97 | if data_type == 1 then -- luacheck: ignore 542 98 | -- Ignore for pointers 99 | elseif data_size == 29 then 100 | -- If the value is 29, then the size is 29 + the next byte 101 | -- after the type specifying bytes as an unsigned integer. 102 | data_size = 29 + self.contents:byte(base + offset) 103 | offset = offset + 1 104 | elseif data_size == 30 then 105 | -- If the value is 30, then the size is 285 + the next two bytes 106 | -- after the type specifying bytes as a single unsigned integer. 107 | local hi, lo = self.contents:byte(base + offset, base + offset+1) 108 | offset = offset + 2 109 | data_size = 285 + hi*256 + lo 110 | elseif data_size == 31 then 111 | -- If the value is 31, then the size is 65,821 + the next three bytes 112 | -- after the type specifying bytes as a single unsigned integer. 113 | local o1, o2, o3, o4 = self.contents:byte(base + offset, base + offset+3) 114 | offset = offset + 4 115 | data_size = 65821 + o1*16777216 + o2*65536 + o3*256 + o4 116 | end 117 | 118 | return func(self, base, offset, data_size) 119 | end 120 | 121 | function geodb_methods:read_pointer(base, offset, magic) 122 | local size = math.floor(magic/8) 123 | local pointer 124 | if size == 0 then 125 | -- If the size is 0, the pointer is built by appending the next 126 | -- byte to the last three bits to produce an 11-bit value. 127 | local o1 = self.contents:byte(base + offset) 128 | offset = offset + 1 129 | pointer = (magic % 8)*256 + o1 130 | elseif size == 1 then 131 | -- If the size is 1, the pointer is built by appending the next 132 | -- two bytes to the last three bits to produce a 19-bit value + 2048. 133 | local o1, o2 = self.contents:byte(base + offset, base + offset + 1) 134 | offset = offset + 2 135 | pointer = (magic % 8)*65536 + o1*256 + o2 + 2048 136 | elseif size == 2 then 137 | -- If the size is 2, the pointer is built by appending the next 138 | -- three bytes to the last three bits to produce a 27-bit value + 526336. 139 | local o1, o2, o3 = self.contents:byte(base + offset, base + offset + 2) 140 | offset = offset + 3 141 | pointer = (magic % 8)*16777216 + o1*65536 + o2*256 + o3 + 526336 142 | elseif size == 3 then 143 | -- Finally, if the size is 3, the pointer's value is contained in 144 | -- the next four bytes as a 32-bit value. 145 | -- In this case, the last three bits of the control byte are ignored. 146 | local o1, o2, o3, o4 = self.contents:byte(base + offset, base + offset+3) 147 | offset = offset + 4 148 | pointer = o1*16777216 + o2*65536 + o3*256 + o4 149 | end 150 | local _, val = self:read_data(base, pointer) 151 | return offset, val 152 | end 153 | data_types[1] = geodb_methods.read_pointer -- Pointer 154 | 155 | function geodb_methods:read_string(base, offset, length) 156 | return offset + length, self.contents:sub(base + offset, base + offset + length - 1) 157 | end 158 | 159 | data_types[2] = geodb_methods.read_string -- UTF-8 160 | data_types[4] = geodb_methods.read_string -- Binary 161 | 162 | function geodb_methods:read_double(base, offset, length) 163 | assert(length == 8, "double of non-8 length") 164 | return offset + 8, sunpack(">d", self.contents, base + offset) 165 | end 166 | data_types[3] = geodb_methods.read_double -- Double 167 | 168 | function geodb_methods:read_float(base, offset, length) 169 | assert(length == 4, "float of non-4 length") 170 | return offset + 4, sunpack(">f", self.contents, base + offset) 171 | end 172 | data_types[15] = geodb_methods.read_float -- Float 173 | 174 | -- Integer types 175 | -- The number of bytes used is determined by the length specifier in the control byte. 176 | 177 | -- General function 178 | function geodb_methods:read_unsigned(base, offset, length) 179 | if length == 0 then return offset, 0 end 180 | return offset + length, sunpack(">I" .. length, self.contents, base + offset) 181 | end 182 | function geodb_methods:read_signed(base, offset, length) 183 | if length == 0 then return offset, 0 end 184 | return offset + length, sunpack(">i" .. length, self.contents, base + offset) 185 | end 186 | 187 | data_types[5] = geodb_methods.read_unsigned -- unsigned 16-bit int 188 | data_types[6] = geodb_methods.read_unsigned -- unsigned 32-bit int 189 | data_types[8] = geodb_methods.read_signed -- signed 32-bit int 190 | data_types[9] = geodb_methods.read_unsigned -- unsigned 64-bit int 191 | data_types[10] = geodb_methods.read_unsigned -- unsigned 128-bit int 192 | 193 | -- Specialise if we have ffi 194 | if has_ffi and has_bit then 195 | local const_char_a = ffi.typeof("const char*") 196 | local buff = ffi.new("char[8]") 197 | local uint16_p = ffi.typeof("uint16_t*") 198 | local uint32_p = ffi.typeof("uint32_t*") 199 | local int32_p = ffi.typeof("int32_t*") 200 | local uint64_p = ffi.typeof("uint64_t*") 201 | if ffi.abi("le") then 202 | function geodb_methods:read_uint16(base, offset, length) 203 | local src = ffi.cast(const_char_a, self.contents) + base + offset - 1 204 | ffi.copy(buff+4-length, src, length) 205 | local x = ffi.cast(uint32_p, buff)[0] 206 | ffi.fill(buff+4-length, length) 207 | x = bit.bswap(x) 208 | return offset + length, x 209 | end 210 | function geodb_methods:read_uint32(base, offset, length) 211 | local src = ffi.cast(const_char_a, self.contents) + base + offset - 1 212 | ffi.copy(buff+4-length, src, length) 213 | local x = ffi.cast(uint32_p, buff)[0] 214 | ffi.fill(buff+4-length, length) 215 | x = bit.bswap(x) 216 | return offset + length, x 217 | end 218 | function geodb_methods:read_int32(base, offset, length) 219 | local src = ffi.cast(const_char_a, self.contents) + base + offset - 1 220 | ffi.copy(buff+4-length, src, length) 221 | local x = ffi.cast(int32_p, buff)[0] 222 | ffi.fill(buff+4-length, length) 223 | x = bit.bswap(x) 224 | return offset + length, x 225 | end 226 | function geodb_methods:read_uint64(base, offset, length) 227 | local src = ffi.cast(const_char_a, self.contents) + base + offset - 1 228 | ffi.copy(buff+8-length, src, length) 229 | -- Do uint64 in two parts; as we only have a 32bit swap operation 230 | local as_u32 = ffi.cast(uint32_p, buff) 231 | as_u32[0], as_u32[1] = bit.bswap(as_u32[1]), bit.bswap(as_u32[0]) 232 | local x = ffi.cast(uint64_p, buff)[0] 233 | ffi.fill(buff, length) -- Data has been moved; no increment of buff 234 | return offset + length, x 235 | end 236 | else 237 | function geodb_methods:read_uint16(base, offset, length) 238 | local src = ffi.cast(const_char_a, self.contents) + base + offset - 1 239 | ffi.copy(buff+2-length, src, length) 240 | local x = ffi.cast(uint16_p, buff)[0] 241 | ffi.fill(buff+2-length, length) 242 | return offset + length, x 243 | end 244 | function geodb_methods:read_uint32(base, offset, length) 245 | local src = ffi.cast(const_char_a, self.contents) + base + offset - 1 246 | ffi.copy(buff+4-length, src, length) 247 | local x = ffi.cast(uint32_p, buff)[0] 248 | ffi.fill(buff+4-length, length) 249 | return offset + length, x 250 | end 251 | function geodb_methods:read_int32(base, offset, length) 252 | local src = ffi.cast(const_char_a, self.contents) + base + offset - 1 253 | ffi.copy(buff+4-length, src, length) 254 | local x = ffi.cast(int32_p, buff)[0] 255 | ffi.fill(buff+4-length, length) 256 | return offset + length, x 257 | end 258 | function geodb_methods:read_uint64(base, offset, length) 259 | local src = ffi.cast(const_char_a, self.contents) + base + offset - 1 260 | ffi.copy(buff+8-length, src, length) 261 | local x = ffi.cast(uint64_p, buff)[0] 262 | ffi.fill(buff+8-length, length) 263 | return offset + length, x 264 | end 265 | end 266 | data_types[5] = geodb_methods.read_uint16 -- unsigned 16-bit int 267 | data_types[6] = geodb_methods.read_uint32 -- unsigned 32-bit int 268 | data_types[8] = geodb_methods.read_int32 -- signed 32-bit int 269 | data_types[9] = geodb_methods.read_uint64 -- unsigned 64-bit int 270 | -- data_types[10] = geodb_methods.read_unsigned -- unsigned 128-bit int 271 | end 272 | 273 | function geodb_methods:read_map(base, offset, n_pairs) -- Map 274 | local map = {} 275 | for _ = 1, n_pairs do 276 | local key, val 277 | offset, key = self:read_data(base, offset) 278 | assert(type(key) == "string") 279 | offset, val = self:read_data(base, offset) 280 | map[key] = val 281 | end 282 | return offset, map 283 | end 284 | data_types[7] = geodb_methods.read_map 285 | 286 | function geodb_methods:read_array(base, offset, n_items) -- Array 287 | local array = {} 288 | for i = 1, n_items do 289 | local val 290 | offset, val = self:read_data(base, offset) 291 | array[i] = val 292 | end 293 | return offset, array 294 | end 295 | data_types[11] = geodb_methods.read_array 296 | 297 | -- End Marker 298 | data_types[13] = function(self, base, offset, zero) -- luacheck: ignore 212 299 | return nil 300 | end 301 | 302 | -- Boolean 303 | -- The length information for a boolean type will always be 0 or 1, 304 | -- indicating the value. There is no payload for this field. 305 | data_types[14] = function(self, base, offset, length) -- luacheck: ignore 212 306 | return offset, length == 1 307 | end 308 | 309 | getters[24] = { 310 | left = function(self, offset) 311 | local o1, o2, o3 = self.contents:byte(offset, offset + 2) 312 | return o1*65536 + o2*256 + o3 313 | end; 314 | right = function(self, offset) 315 | local o1, o2, o3 = self.contents:byte(offset + 3, offset + 5) 316 | return o1*65536 + o2*256 + o3 317 | end; 318 | record_length = 6; 319 | } 320 | getters[28] = { 321 | left = function(self, offset) 322 | local o1, o2, o3, o4 = self.contents:byte(offset, offset + 3) 323 | return math.floor(o4 / 16)*16777216 + o1*65536 + o2*256 + o3 324 | end; 325 | right = function(self, offset) 326 | local o1, o2, o3, o4 = self.contents:byte(offset + 3, offset + 6) 327 | return (o1 % 16)*16777216 + o2*65536 + o3*256 + o4 328 | end; 329 | record_length = 7; 330 | } 331 | getters[32] = { 332 | left = function(self, offset) 333 | local o1, o2, o3, o4 = self.contents:byte(offset, offset + 3) 334 | return o1*16777216 + o2*65536 + o3*256 + o4 335 | end; 336 | right = function(self, offset) 337 | local o1, o2, o3, o4 = self.contents:byte(offset + 4, offset + 7) 338 | return o1*16777216 + o2*65536 + o3*256 + o4 339 | end; 340 | record_length = 8; 341 | } 342 | function geodb_methods:search(bits, node) 343 | node = node or 0 344 | local seen = { [node] = true } 345 | for _, direction in ipairs(bits) do 346 | local offset = node * self.record_length + 1 347 | local record_value 348 | if direction then 349 | record_value = self:right(offset) 350 | else 351 | record_value = self:left(offset) 352 | end 353 | 354 | if seen[record_value] then 355 | error("Cyclical tree") 356 | end 357 | seen[record_value] = true 358 | 359 | if record_value == self.data.node_count then 360 | -- If the record value is equal to the number of nodes, that means that 361 | -- we do not have any data for the IP address, and the search ends here. 362 | return nil 363 | elseif record_value > self.data.node_count then 364 | -- If the record value is greater than the number of nodes in the search tree, 365 | -- then it is an actual pointer value pointing into the data section. 366 | -- The value of the pointer is calculated from the start of the data section, 367 | -- not from the start of the file. 368 | local data_offset = record_value - self.data.node_count - 16 369 | local _, res = self:read_data(self.start_data, data_offset) 370 | return node, res 371 | else 372 | node = record_value 373 | end 374 | end 375 | return node 376 | end 377 | 378 | do -- Use IPv4-mapped IPv6 addresses located at ::ffff:/80 379 | local bits = {} 380 | for i = 1, 80 do 381 | bits[i] = false 382 | end 383 | for i = 81, 96 do 384 | bits[i] = true 385 | end 386 | function geodb_methods:ipv6_find_ipv4_start() 387 | return self:search(bits, 0) 388 | end 389 | end 390 | 391 | local function ipv4_to_bit_array(str) 392 | local o1, o2, o3, o4 = str:match("(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)") 393 | assert(o1, "invalid IPv4 address") 394 | o1 = tonumber(o1, 10) 395 | o2 = tonumber(o2, 10) 396 | o3 = tonumber(o3, 10) 397 | o4 = tonumber(o4, 10) 398 | assert(o1 <= 255 and o2 <= 255 and o3 <= 255 and o4 <= 255, "invalid IPv4 address") 399 | return { 400 | math.floor(o1 / 128) % 2 == 1; 401 | math.floor(o1 / 64) % 2 == 1; 402 | math.floor(o1 / 32) % 2 == 1; 403 | math.floor(o1 / 16) % 2 == 1; 404 | math.floor(o1 / 8) % 2 == 1; 405 | math.floor(o1 / 4) % 2 == 1; 406 | math.floor(o1 / 2) % 2 == 1; 407 | o1 % 2 == 1; 408 | math.floor(o2 / 128) % 2 == 1; 409 | math.floor(o2 / 64) % 2 == 1; 410 | math.floor(o2 / 32) % 2 == 1; 411 | math.floor(o2 / 16) % 2 == 1; 412 | math.floor(o2 / 8) % 2 == 1; 413 | math.floor(o2 / 4) % 2 == 1; 414 | math.floor(o2 / 2) % 2 == 1; 415 | o2 % 2 == 1; 416 | math.floor(o3 / 128) % 2 == 1; 417 | math.floor(o3 / 64) % 2 == 1; 418 | math.floor(o3 / 32) % 2 == 1; 419 | math.floor(o3 / 16) % 2 == 1; 420 | math.floor(o3 / 8) % 2 == 1; 421 | math.floor(o3 / 4) % 2 == 1; 422 | math.floor(o3 / 2) % 2 == 1; 423 | o3 % 2 == 1; 424 | math.floor(o4 / 128) % 2 == 1; 425 | math.floor(o4 / 64) % 2 == 1; 426 | math.floor(o4 / 32) % 2 == 1; 427 | math.floor(o4 / 16) % 2 == 1; 428 | math.floor(o4 / 8) % 2 == 1; 429 | math.floor(o4 / 4) % 2 == 1; 430 | math.floor(o4 / 2) % 2 == 1; 431 | o4 % 2 == 1; 432 | } 433 | end 434 | 435 | function geodb_methods:search_ipv4(str) 436 | return select(2, self:search(ipv4_to_bit_array(str), self.ipv4_start)) 437 | end 438 | 439 | local function ipv6_split(str) 440 | local components = {} 441 | local n = 0 442 | for u16 in str:gmatch("(%x%x?%x?%x?):?") do 443 | n = n + 1 444 | u16 = tonumber(u16, 16) 445 | assert(u16, "invalid IPv6 address") 446 | components[n] = u16 447 | end 448 | return components, n 449 | end 450 | 451 | local function ipv6_to_bit_array(str) 452 | local a, b = str:match("^([%x:]-)::([%x:]*)$") 453 | local components, n = ipv6_split(a or str) 454 | if a ~= nil then 455 | local end_components, m = ipv6_split(b) 456 | assert(m+n <= 7, "invalid IPv6 address") 457 | for i = n+1, 8-m do 458 | components[i] = 0 459 | end 460 | for i = 8-m+1, 8 do 461 | components[i] = end_components[i-8+m] 462 | end 463 | else 464 | assert(n == 8, "invalid IPv6 address") 465 | end 466 | -- Now components is an array of 16bit components 467 | local bits = {} 468 | for i = 1, 8 do 469 | local u16 = components[i] 470 | for j = 1, 16 do 471 | bits[(i-1)*16+j] = math.floor(u16 / 2^(16-j)) % 2 == 1 472 | end 473 | end 474 | return bits 475 | end 476 | 477 | function geodb_methods:search_ipv6(str) 478 | return select(2, self:search(ipv6_to_bit_array(str))) 479 | end 480 | 481 | return { 482 | new = new; 483 | read = read; 484 | open = open; -- Deprecated 485 | } 486 | --------------------------------------------------------------------------------