Module:IPblock
Appearance
-- Calculate the minimum-sized block of IP addresses that covers each
-- IPv4 address entered in the arguments.
local bit32 = require('bit32')
local function collection()
-- Return a table to hold items.
return {
n = 0,
add = function (self, item)
self.n = self.n + 1
self[self.n] = item
end,
join = function (self, sep)
return table.concat(self, sep)
end,
sort = function (self, comp)
table.sort(self, comp)
end,
}
end
local function ipv4_string(ip_num)
-- Return a quad-dotted string equivalent to given 32-bit number.
local floor = math.floor
local quad = {}
for i = 1, 4 do
quad[5 - i] = tostring(ip_num % 256)
ip_num = floor(ip_num / 256)
end
return table.concat(quad, '.')
end
local function ipv4_address(ip_str)
-- Return number equivalent to IPv4 address given as a quad-dotted string, or
-- return nil if invalid.
-- A redundant leading zero is an error because it is for an IP in octal.
local quad = collection()
local s = ip_str:match('^%s*(.-)%s*$') .. '.'
for item in s:gmatch('(.-)%.') do
quad:add(item)
end
if quad.n == 4 then
local result = 0
for _, s in ipairs(quad) do
if s:match('^%d+$') then
local num = tonumber(s)
if num == 0 then
result = result * 256
elseif 0 < num and num <= 255 then
if s:match('^0') then
return nil
end
result = result * 256 + num
else
return nil
end
else
return nil
end
end
return result
end
return nil
end
local function ipv4_cidr(ipnums, from_size)
-- Return prefix, size (two numbers) for the smallest range that covers each IP,
-- starting with the given size (a number from 0 to 32), or
-- return nil if no single range works.
local arshift = bit32.arshift
local band = bit32.band
local result
for size = from_size, 32 do
local prefix
local mask = arshift(0x80000000, size - 1)
for i, v in ipairs(ipnums) do
if i == 1 then
prefix = band(v, mask)
else
if prefix ~= band(v, mask) then
return result, size - 1
end
end
end
result = prefix
end
return result, 32
end
local function IPblock(frame)
-- Return wikitext to display the smallest IPv4 CIDR range that covers
-- each IPv4 address given in the arguments, or error text.
-- An IP starting with '#' is ignored (commented out).
local pframe = frame:getParent()
local args = pframe.args
local ipnums = collection()
for i, arg in ipairs(args) do
for line in string.gmatch(arg .. '\n', '[\t ]*(.-)[\t\r ]*\n') do
if line ~= '' and line:sub(1, 1) ~= '#' then
local ip = ipv4_address(line)
if ip then
ipnums:add(ip)
else
error(string.format('Invalid IPv4 address: argument %d (%s)', i, line), 0)
end
end
end
end
if ipnums.n < 1 then
error('Need an IPv4 address', 0)
end
ipnums:sort()
local lines = collection()
lines:add('Sorted IP addresses:') -- at lines[1]
local previous
local dupcount = 0
for i, v in ipairs(ipnums) do
if previous == v then -- omit duplicates
dupcount = dupcount + 1
else
lines:add('#' .. ipv4_string(v))
end
previous = v
end
if dupcount > 0 then
lines[1] = 'Sorted IP addresses (after omitting some duplicates):'
end
lines:add('')
local from_size = tonumber(args.size) or 16
local prefix, size = ipv4_cidr(ipnums, from_size)
local text
if prefix then
if size == 32 then
text = 'Single IP address = ' .. ipv4_string(prefix)
else
text = string.format('Range (covers %d IP addresses) = %s', 2^(32 - size), ipv4_string(prefix) .. '/' .. size)
end
else
text = string.format('Error: No single CIDR range with size %d or more bits covers the IP addresses.', from_size)
end
lines:add(text)
return lines:join('\n')
end
return { IPblock = IPblock }