Jump to content

Module:IPblock

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Johnuniq (talk | contribs) at 09:37, 13 December 2014 (accept '#' to comment-out IP addresses for easy experiments). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
-- 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 }