Module:IP

-- IP library -- This library contains classes for working with IP addresses and IP ranges.

-- Load modules local bit32 = require('bit32') local libraryUtil = require('libraryUtil') local checkType = libraryUtil.checkType

-- Constants local V4 = 'IPv4' local V6 = 'IPv6'

-- Functions from Module:IPblock follow. -- TODO Massage following for style consistent with this module.

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 copyChanged(parts, down) -- Return a copy of IPv4 or IPv6 parts, incremented or decremented. -- Will wraparound: --  increment 255.255.255.255 → 0.0.0.0 --            ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff → :: --  decrement 0.0.0.0 → 255.255.255.255 --            :: → ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff local result = { n = parts.n } local carry = down and 0xffff or 1 for i = parts.n, 1, -1 do		local sum = parts[i] + carry if sum >= 0x10000 then carry = down and 0x10000 or 1 sum = sum - 0x10000 else carry = down and 0xffff or 0 end result[i] = sum end return result end

local function copyPrefix(parts, length) -- Return a copy of IPv4 or IPv6 parts, masked to length. local result = { n = parts.n } for i = 1, parts.n do		if length > 0 then if length >= 16 then result[i] = parts[i] length = length - 16 else result[i] = bit32.band(parts[i],					bit32.arshift(0xffff8000, length - 1)) length = 0 end else result[i] = 0 end end return result end

local function setHostBits(parts, length) -- Return a copy of IPv4 or IPv6 parts, with the least-significant bits -- (host bits) set to 1. -- The most-significant length bits identify the network. local bits = parts.n * 16 local width if length <= 0 then width = bits elseif length >= bits then width = 0 else width = bits - length end local result = { n = parts.n } for i = parts.n, 1, -1 do		if width > 0 then if width >= 16 then result[i] = 0xffff width = width - 16 else result[i] = bit32.replace(parts[i], 0xffff, width - 1, width) width = 0 end else result[i] = parts[i] end end return result end

local function ipv6String(ip) -- Return a string equivalent to the given IPv6 address. local z1, z2 -- indices of run of zeros to be displayed as "::" local zstart, zcount for i = 1, 9 do -- Find left-most occurrence of longest run of two or more zeros. if i < 9 and ip[i] == 0 then if zstart then zcount = zcount + 1 else zstart = i				zcount = 1 end else if zcount and zcount > 1 then if not z1 or zcount > z2 - z1 + 1 then z1 = zstart z2 = zstart + zcount - 1 end end zstart = nil zcount = nil end end local parts = collection for i = 1, 8 do		if z1 and z1 <= i and i <= z2 then if i == z1 then if z1 == 1 or z2 == 8 then if z1 == 1 and z2 == 8 then return '::' end parts:add(':') else parts:add('') end end else parts:add(string.format('%x', ip[i])) end end return table.concat(parts, ':') end

local function ipString(ip) -- Return a string equivalent to given IP address (IPv4 or IPv6). if ip.n == 2 then -- IPv4. local parts = {} for i = 1, 2 do			local w = ip[i] local q = i == 1 and 1 or 3 parts[q] = math.floor(w / 256) parts[q+1] = w % 256 end return table.concat(parts, '.') end return ipv6String(ip) end

-- IPAddress class -- Represents a single IPv4 or IPv6 address.

local IPAddress = {}

do local dataKey = {} -- A unique key to access objects' internal data.

-- Private static methods local function parseIPv4(ipStr) -- If ipStr is a valid IPv4 string, return a collection of its parts. -- Otherwise, return nil. -- This representation is for compatibility with IPv6 addresses. local octets = collection local s = ipStr:match('^%s*(.-)%s*$') .. '.'		for item in s:gmatch('(.-)%.') do			octets:add(item) end if octets.n == 4 then for i, s in ipairs(octets) do				if s:match('^%d+$') then local num = tonumber(s) if 0 <= num and num <= 255 then if num > 0 and s:match('^0') then -- A redundant leading zero is for an IP in octal. return false end octets[i] = num else return false end else return false end end local parts = collection for i = 1, 3, 2 do				parts:add(octets[i] * 256 + octets[i+1]) end return parts end return nil end

local function parseIPv6(ipStr) -- If ipStr is a valid IPv6 string, return a collection of its parts. -- Otherwise, return nil. ipStr = ipStr:match('^%s*(.-)%s*$') local _, n = ipStr:gsub(':', ':') if n < 7 then ipStr, n = ipStr:gsub('::', string.rep(':', 9 - n)) end local parts = collection for item in (ipStr .. ':'):gmatch('(.-):') do			parts:add(item) end if parts.n == 8 then for i, s in ipairs(parts) do				if s == '' then parts[i] = 0 else local num = tonumber('0x' .. s)					if num and 0 <= num and num <= 65535 then parts[i] = num else return false end end end return parts end return nil end

-- Metamethods that don't need upvalues local function ipEquals(ip1, ip2) local lhs = ip1[dataKey].parts local rhs = ip2[dataKey].parts if lhs.n == rhs.n then for i = 1, lhs.n do				if lhs[i] ~= rhs[i] then return false end end return true end return false end

local function ipLessThan(ip1, ip2) local lhs = ip1[dataKey].parts local rhs = ip2[dataKey].parts if lhs.n == rhs.n then for i = 1, lhs.n do				if lhs[i] ~= rhs[i] then return lhs[i] < rhs[i] end end return false end return lhs.n < rhs.n	end

local function concatIPs(ip1, ip2) return tostring(ip1) .. tostring(ip2) end

local function ipToString(ip) return ipString(ip[dataKey].parts) end

-- Constructor function IPAddress.new(ip) checkType('IPAddress.new', 1, ip, 'string')

-- Set up structure local obj = {} local data = {}

-- Set initial values data.parts = parseIPv4(ip) or parseIPv6(ip) if not data.parts then error('invalid IP', 2) end data.version = data.parts.n == 2 and V4 or V6

-- Public methods function obj:getIP return ipString(data.parts) end

function obj:getVersion return data.version end

function obj:getHighestIP(bitLength) return IPAddress.new(uniqueId, setHostBits(data.parts, bitLength)) end

function obj:getPrefix(bitLength) return IPAddress.new(uniqueId, copyPrefix(data.parts, bitLength)) end

function obj:isIPv4 return data.version == V4		end

function obj:isIPv6 return data.version == V6		end

function obj:isInSubnet(subnet) -- TODO Consider alternative of checking: --  (ipFirst <= self and self <= ipLast) if self:getVersion == subnet:getVersion then local prefix = self:getPrefix(subnet:getBitLength) return prefix == subnet:getPrefix end return false end

function obj:getNextIP return IPAddress.new(uniqueId, copyChanged(data.parts)) end

function obj:getPreviousIP return IPAddress.new(uniqueId, copyChanged(data.parts, true)) end

-- Metamethods return setmetatable(obj, {			__eq = ipEquals,			__lt = ipLessThan,			__concat = concatIPs,			__tostring = ipToString,			__index = function (self, key)				if key == dataKey then					return data				else					return IPAddress[key]				end			end,		}) end end

local function makeSubnet(data, cidrStr) -- If cidrStr is a valid IPv4 or IPv6 CIDR specification, store its parts -- in data and return true. Otherwise, return false. local lhs, rhs = cidrStr:match('^%s*(.-)/(%d+)%s*$') if lhs then local bits = lhs:find(':', 1, true) and 128 or 32 local n = tonumber(rhs) if n and n <= bits then local base = IPAddress.new(lhs) local prefix = base:getPrefix(n) if base == prefix then data.parts = base:getIPParts data.bitLength = n				data.prefix = prefix data.highestIP = base:getHighestIP(n) return true end end end return false end

-- Subnet class -- Represents a block of IPv4 or IPv6 addresses.

local Subnet = {}

do -- Initialize metatable local mt = {}

-- Constructor function Subnet.new(cidr) -- Set up structure local obj = setmetatable({}, mt) local data = {}

-- Public methods function obj:getPrefix return data.prefix end

function obj:getHighestIP return data.highestIP end

function obj:getBitLength return data.bitLength end

function obj:getCIDR return string.format('%s/%d', self:getPrefix, self:getBitLength) end

function obj:getVersion return data.version end

function obj:isIPv4 return data.version == V4		end

function obj:isIPv6 return data.version == V6		end

function obj:containsIP(ip) -- TODO See ip:isInSubnet(subnet); use this technique there? if self:getVersion == ip:getVersion then return self:getPrefix <= ip and ip <= self:getHighestIP end return false end

function obj:overlapsSubnet(subnet) if self:getVersion == subnet:getVersion then return (					subnet:getHighestIP >= self:getPrefix and					subnet:getPrefix <= self:getHighestIP				) end return false end

-- Set initial values checkType('Subnet.new', 1, cidr, 'string') if not makeSubnet(data, cidr) then error('invalid CIDR', 2) end data.version = data.parts.n == 2 and V4 or V6

return obj end

-- Metamethods function mt:__eq(obj) return self:getCIDR == obj:getCIDR end

function mt:__tostring return self:getCIDR end end

return { IPAddress = IPAddress, Subnet = Subnet, }