Deprecated: Use of User::getOption was deprecated in MediaWiki 1.35. [Called from SkinScratchWikiSkin::onOutputPageBodyAttributes in /var/www/html/mediawiki/skins/ScratchWikiSkin2/ScratchWikiSkin.skin.php at line 81] in /var/www/html/mediawiki/includes/debug/MWDebug.php on line 381

Deprecated: Use of QuickTemplate::(get/html/text/haveData) with parameter `headelement` was deprecated in MediaWiki 1.39. [Called from QuickTemplate::get in /var/www/html/mediawiki/includes/skins/QuickTemplate.php at line 144] in /var/www/html/mediawiki/includes/debug/MWDebug.php on line 381

Deprecated: Use of BaseTemplate::getTrail was deprecated in MediaWiki 1.39. [Called from MediaWiki\Skins\Cosmos\CosmosTemplate::execute in /var/www/html/mediawiki/skins/Cosmos/includes/CosmosTemplate.php at line 82] in /var/www/html/mediawiki/includes/debug/MWDebug.php on line 381

Deprecated: Use of QuickTemplate::(get/html/text/haveData) with parameter `bottomscripts` was deprecated in MediaWiki 1.39. [Called from QuickTemplate::get in /var/www/html/mediawiki/includes/skins/QuickTemplate.php at line 144] in /var/www/html/mediawiki/includes/debug/MWDebug.php on line 381
Module:IP - Pi Server Wiki
×
Create or edit a page
Write your page title here:
There are currently 14 pages on the wiki. Enter a name above to create a page, or check out the list of broken links.
Warning: Users creating spam pages will be blocked and have their pages moved to the trash.



    Pi Server Wiki

    Module:IP

    Documentation for this module may be created at Module:IP/doc

    -- IP library
    -- This library contains classes for working with IP addresses and IP ranges.
    
    -- Load modules
    require('Module:No globals')
    local bit32 = require('bit32')
    local libraryUtil = require('libraryUtil')
    local checkType = libraryUtil.checkType
    local checkTypeMulti = libraryUtil.checkTypeMulti
    local makeCheckSelfFunction = libraryUtil.makeCheckSelfFunction
    
    -- Constants
    local V4 = 'IPv4'
    local V6 = 'IPv6'
    
    --------------------------------------------------------------------------------
    -- Helper functions
    --------------------------------------------------------------------------------
    
    local function makeValidationFunction(className, isObjectFunc)
    	-- Make a function for validating a specific object.
    	return function (methodName, argIdx, arg)
    		if not isObjectFunc(arg) then
    			error(string.format(
    				"bad argument #%d to '%s' (not a valid %s object)",
    				argIdx, methodName, className
    			), 3)
    		end
    	end
    end
    
    --------------------------------------------------------------------------------
    -- Collection class
    -- This is a table used to hold items.
    --------------------------------------------------------------------------------
    
    local Collection = {}
    Collection.__index = Collection
    
    function Collection:add(item)
    	if item ~= nil then
    		self.n = self.n + 1
    		self[self.n] = item
    	end
    end
    
    function Collection:join(sep)
    	return table.concat(self, sep)
    end
    
    function Collection:remove(pos)
    	if self.n > 0 and (pos == nil or (0 < pos and pos <= self.n)) then
    		self.n = self.n - 1
    		return table.remove(self, pos)
    	end
    end
    
    function Collection:sort(comp)
    	table.sort(self, comp)
    end
    
    function Collection:deobjectify()
    	-- Turns the collection into a plain array without any special properties
    	-- or methods.
    	self.n = nil
    	setmetatable(self, nil)
    end
    
    function Collection.new()
    	return setmetatable({n = 0}, Collection)
    end
    
    --------------------------------------------------------------------------------
    -- RawIP class
    -- Numeric representation of an IPv4 or IPv6 address. Used internally.
    -- A RawIP object is constructed by adding data to a Collection object and
    -- then giving it a new metatable. This is to avoid the memory overhead of
    -- copying the data to a new table.
    --------------------------------------------------------------------------------
    
    local RawIP = {}
    RawIP.__index = RawIP
    
    -- Constructors
    function RawIP.newFromIPv4(ipStr)
    	-- Return a RawIP object if ipStr is a valid IPv4 string. Otherwise,
    	-- return nil.
    	-- This representation is for compatibility with IPv6 addresses.
    	local octets = Collection.new()
    	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 nil
    					end
    					octets[i] = num
    				else
    					return nil
    				end
    			else
    				return nil
    			end
    		end
    		local parts = Collection.new()
    		for i = 1, 3, 2 do
    			parts:add(octets[i] * 256 + octets[i+1])
    		end
    		return setmetatable(parts, RawIP)
    	end
    	return nil
    end
    
    function RawIP.newFromIPv6(ipStr)
    	-- Return a RawIP object if ipStr is a valid IPv6 string. Otherwise,
    	-- return nil.
    	ipStr = ipStr:match('^%s*(.-)%s*$')
    	local _, n = ipStr:gsub(':', ':')
    	if n < 7 then
    		ipStr = ipStr:gsub('::', string.rep(':', 9 - n))
    	end
    	local parts = Collection.new()
    	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
    				if s:match('^%x+$') then
    					local num = tonumber(s, 16)
    					if num and 0 <= num and num <= 65535 then
    						parts[i] = num
    					else
    						return nil
    					end
    				else
    					return nil
    				end
    			end
    		end
    		return setmetatable(parts, RawIP)
    	end
    	return nil
    end
    
    function RawIP.newFromIP(ipStr)
    	-- Return a new RawIP object from either an IPv4 string or an IPv6
    	-- string. If ipStr is not a valid IPv4 or IPv6 string, then return
    	-- nil.
    	return RawIP.newFromIPv4(ipStr) or RawIP.newFromIPv6(ipStr)
    end
    
    -- Methods
    function RawIP:getVersion()
    	-- Return a string with the version of the IP protocol we are using.
    	return self.n == 2 and V4 or V6
    end
    
    function RawIP:isIPv4()
    	-- Return true if this is an IPv4 representation, and false otherwise.
    	return self.n == 2
    end
    
    function RawIP:isIPv6()
    	-- Return true if this is an IPv6 representation, and false otherwise.
    	return self.n == 8
    end
    
    function RawIP:getBitLength()
    	-- Return the bit length of the IP address.
    	return self.n * 16
    end
    
    function RawIP:getAdjacent(previous)
    	-- Return a RawIP object for an adjacent IP address. If previous is true
    	-- then the previous IP is returned; otherwise the next IP is returned.
    	-- Will wraparound:
    	--   next      255.255.255.255 → 0.0.0.0
    	--             ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff → ::
    	--   previous  0.0.0.0 → 255.255.255.255
    	--             :: → ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
    	local result = Collection.new()
    	result.n = self.n
    	local carry = previous and 0xffff or 1
    	for i = self.n, 1, -1 do
    		local sum = self[i] + carry
    		if sum >= 0x10000 then
    			carry = previous and 0x10000 or 1
    			sum = sum - 0x10000
    		else
    			carry = previous and 0xffff or 0
    		end
    		result[i] = sum
    	end
    	return setmetatable(result, RawIP)
    end
    
    function RawIP:getPrefix(bitLength)
    	-- Return a RawIP object for the prefix of the current IP Address with a
    	-- bit length of bitLength.
    	local result = Collection.new()
    	result.n = self.n
    	for i = 1, self.n do
    		if bitLength > 0 then
    			if bitLength >= 16 then
    				result[i] = self[i]
    				bitLength = bitLength - 16
    			else
    				result[i] = bit32.replace(self[i], 0, 0, 16 - bitLength)
    				bitLength = 0
    			end
    		else
    			result[i] = 0
    		end
    	end
    	return setmetatable(result, RawIP)
    end
    
    function RawIP:getHighestHost(bitLength)
    	-- Return a RawIP object for the highest IP with the prefix of length
    	-- bitLength. In other words, the network (the most-significant bits)
    	-- is the same as the current IP's, but the host bits (the
    	-- least-significant bits) are all set to 1.
    	local bits = self.n * 16
    	local width
    	if bitLength <= 0 then
    		width = bits
    	elseif bitLength >= bits then
    		width = 0
    	else
    		width = bits - bitLength
    	end
    	local result = Collection.new()
    	result.n = self.n
    	for i = self.n, 1, -1 do
    		if width > 0 then
    			if width >= 16 then
    				result[i] = 0xffff
    				width = width - 16
    			else
    				result[i] = bit32.replace(self[i], 0xffff, 0, width)
    				width = 0
    			end
    		else
    			result[i] = self[i]
    		end
    	end
    	return setmetatable(result, RawIP)
    end
    
    function RawIP:_makeIPv6String()
    	-- Return an IPv6 string representation of the object. Behavior is
    	-- undefined if the current object is IPv4.
    	local z1, z2  -- indices of run of zeroes to be displayed as "::"
    	local zstart, zcount
    	for i = 1, 9 do
    		-- Find left-most occurrence of longest run of two or more zeroes.
    		if i < 9 and self[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.new()
    	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', self[i]))
    		end
    	end
    	return parts:join(':')
    end
    
    function RawIP:_makeIPv4String()
    	-- Return an IPv4 string representation of the object. Behavior is
    	-- undefined if the current object is IPv6.
    	local parts = Collection.new()
    	for i = 1, 2 do
    		local w = self[i]
    		parts:add(math.floor(w / 256))
    		parts:add(w % 256)
    	end
    	return parts:join('.')
    end
    
    function RawIP:__tostring()
    	-- Return a string equivalent to given IP address (IPv4 or IPv6).
    	if self.n == 2 then
    		return self:_makeIPv4String()
    	else
    		return self:_makeIPv6String()
    	end
    end
    
    function RawIP:__lt(obj)
    	if self.n == obj.n then
    		for i = 1, self.n do
    			if self[i] ~= obj[i] then
    				return self[i] < obj[i]
    			end
    		end
    		return false
    	end
    	return self.n < obj.n
    end
    
    function RawIP:__eq(obj)
    	if self.n == obj.n then
    		for i = 1, self.n do
    			if self[i] ~= obj[i] then
    				return false
    			end
    		end
    		return true
    	end
    	return false
    end
    
    --------------------------------------------------------------------------------
    -- Initialize private methods available to IPAddress and Subnet
    --------------------------------------------------------------------------------
    
    -- Both IPAddress and Subnet need access to each others' private constructor
    -- functions. IPAddress must be able to make Subnet objects from CIDR strings
    -- and from RawIP objects, and Subnet must be able to make IPAddress objects
    -- from IP strings and from RawIP objects. These constructors must all be
    -- private to ensure correct error levels and to stop other modules from having
    -- to worry about RawIP objects. Because they are private, they must be
    -- initialized here.
    local makeIPAddress, makeIPAddressFromRaw, makeSubnet, makeSubnetFromRaw
    
    -- Objects need to be able to validate other objects that they are passed
    -- as input, so initialize those functions here as well.
    local validateCollection, validateIPAddress, validateSubnet
    
    --------------------------------------------------------------------------------
    -- IPAddress class
    -- Represents a single IPv4 or IPv6 address.
    --------------------------------------------------------------------------------
    
    local IPAddress = {}
    
    do
    	-- dataKey is a unique key to access objects' internal data. This is needed
    	-- to access the RawIP objects contained in other IPAddress objects so that
    	-- they can be compared with the current object's RawIP object. This data
    	-- is not available to other classes or other modules.
    	local dataKey = {}
    
    	-- Private static methods
    	local function isIPAddressObject(val)
    		return type(val) == 'table' and val[dataKey] ~= nil
    	end
    
    	validateIPAddress = makeValidationFunction('IPAddress', isIPAddressObject)
    
    	-- Metamethods that don't need upvalues
    	local function ipEquals(ip1, ip2)
    		return ip1[dataKey].rawIP == ip2[dataKey].rawIP
    	end
    
    	local function ipLessThan(ip1, ip2)
    		return ip1[dataKey].rawIP < ip2[dataKey].rawIP
    	end
    
    	local function concatIP(ip, val)
    		return tostring(ip) .. tostring(val)
    	end
    
    	local function ipToString(ip)
    		return ip:getIP()
    	end
    
    	-- Constructors
    	makeIPAddressFromRaw = function (rawIP)
    		-- Constructs a new IPAddress object from a rawIP object. This function
    		-- is for internal use; it is called by IPAddress.new and from other
    		-- IPAddress methods, and should be available to the Subnet class, but
    		-- should not be available to other modules.
    		assert(type(rawIP) == 'table', 'rawIP was type ' .. type(rawIP) .. '; expected type table')
    
    		-- Set up structure
    		local obj = {}
    		local data = {}
    		data.rawIP = rawIP
    
    		-- A function to check whether methods are called with a valid self
    		-- parameter.
    		local checkSelf = makeCheckSelfFunction(
    			'IP',
    			'ipAddress',
    			obj,
    			'IPAddress object'
    		)
    
    		-- Public methods
    		function obj:getIP()
    			checkSelf(self, 'getIP')
    			return tostring(data.rawIP)
    		end
    
    		function obj:getVersion()
    			checkSelf(self, 'getVersion')
    			return data.rawIP:getVersion()
    		end
    
    		function obj:isIPv4()
    			checkSelf(self, 'isIPv4')
    			return data.rawIP:isIPv4()
    		end
    
    		function obj:isIPv6()
    			checkSelf(self, 'isIPv6')
    			return data.rawIP:isIPv6()
    		end
    
    		function obj:isInCollection(collection)
    			checkSelf(self, 'isInCollection')
    			validateCollection('isInCollection', 1, collection)
    			return collection:containsIP(self)
    		end
    
    		function obj:isInSubnet(subnet)
    			checkSelf(self, 'isInSubnet')
    			local tp = type(subnet)
    			if tp == 'string' then
    				subnet = makeSubnet(subnet)
    			elseif tp == 'table' then
    				validateSubnet('isInSubnet', 1, subnet)
    			else
    				checkTypeMulti('isInSubnet', 1, subnet, {'string', 'table'})
    			end
    			return subnet:containsIP(self)
    		end
    
    		function obj:getSubnet(bitLength)
    			checkSelf(self, 'getSubnet')
    			checkType('getSubnet', 1, bitLength, 'number')
    			if bitLength < 0
    				or bitLength > data.rawIP:getBitLength()
    				or bitLength ~= math.floor(bitLength)
    			then
    				error(string.format(
    					"bad argument #1 to 'getSubnet' (must be an integer between 0 and %d)",
    					data.rawIP:getBitLength()
    				), 2)
    			end
    			return makeSubnetFromRaw(data.rawIP, bitLength)
    		end
    
    		function obj:getNextIP()
    			checkSelf(self, 'getNextIP')
    			return makeIPAddressFromRaw(data.rawIP:getAdjacent())
    		end
    
    		function obj:getPreviousIP()
    			checkSelf(self, 'getPreviousIP')
    			return makeIPAddressFromRaw(data.rawIP:getAdjacent(true))
    		end
    
    		-- Metamethods
    		return setmetatable(obj, {
    			__eq = ipEquals,
    			__lt = ipLessThan,
    			__concat = concatIP,
    			__tostring = ipToString,
    			__index = function (self, key)
    				-- If any code knows the unique data key, allow it to access
    				-- the data table.
    				if key == dataKey then
    					return data
    				end
    			end,
    			__metatable = false, -- don't allow access to the metatable
    		})
    	end
    
    	makeIPAddress = function (ip)
    		local rawIP = RawIP.newFromIP(ip)
    		if not rawIP then
    			error(string.format("'%s' is an invalid IP address", ip), 3)
    		end
    		return makeIPAddressFromRaw(rawIP)
    	end
    
    	function IPAddress.new(ip)
    		checkType('IPAddress.new', 1, ip, 'string')
    		return makeIPAddress(ip)
    	end
    end
    
    --------------------------------------------------------------------------------
    -- Subnet class
    -- Represents a block of IPv4 or IPv6 addresses.
    --------------------------------------------------------------------------------
    
    local Subnet = {}
    
    do
    	-- uniqueKey is a unique, private key used to test whether a given object
    	-- is a Subnet object.
    	local uniqueKey = {}
    
    	-- Metatable
    	local mt = {
    		__index = function (self, key)
    			if key == uniqueKey then
    				return true
    			end
    		end,
    		__eq = function (self, obj)
    			return self:getCIDR() == obj:getCIDR()
    		end,
    		__concat = function (self, obj)
    			return tostring(self) .. tostring(obj)
    		end,
    		__tostring = function (self)
    			return self:getCIDR()
    		end,
    		__metatable = false
    	}
    
    	-- Private static methods
    	local function isSubnetObject(val)
    		-- Return true if val is a Subnet object, and false otherwise.
    		return type(val) == 'table' and val[uniqueKey] ~= nil
    	end
    
    	-- Function to validate subnet objects.
    	-- Params:
    	-- methodName (string) - the name of the method being validated
    	-- argIdx (number) - the position of the argument in the argument list
    	-- arg - the argument to be validated
    	validateSubnet = makeValidationFunction('Subnet', isSubnetObject)
    
    	-- Constructors
    	makeSubnetFromRaw = function (rawIP, bitLength)
    		-- Set up structure
    		local obj = setmetatable({}, mt)
    		local data = {
    			rawIP = rawIP,
    			bitLength = bitLength,
    		}
    
    		-- A function to check whether methods are called with a valid self
    		-- parameter.
    		local checkSelf = makeCheckSelfFunction(
    			'IP',
    			'subnet',
    			obj,
    			'Subnet object'
    		)
    
    		-- Public methods
    		function obj:getPrefix()
    			checkSelf(self, 'getPrefix')
    			if not data.prefix then
    				data.prefix = makeIPAddressFromRaw(
    					data.rawIP:getPrefix(data.bitLength)
    				)
    			end
    			return data.prefix
    		end
    
    		function obj:getHighestIP()
    			checkSelf(self, 'getHighestIP')
    			if not data.highestIP then
    				data.highestIP = makeIPAddressFromRaw(
    					data.rawIP:getHighestHost(data.bitLength)
    				)
    			end
    			return data.highestIP
    		end
    
    		function obj:getBitLength()
    			checkSelf(self, 'getBitLength')
    			return data.bitLength
    		end
    
    		function obj:getCIDR()
    			checkSelf(self, 'getCIDR')
    			return string.format(
    				'%s/%d',
    				tostring(self:getPrefix()), self:getBitLength()
    			)
    		end
    
    		function obj:getVersion()
    			checkSelf(self, 'getVersion')
    			return data.rawIP:getVersion()
    		end
    
    		function obj:isIPv4()
    			checkSelf(self, 'isIPv4')
    			return data.rawIP:isIPv4()
    		end
    
    		function obj:isIPv6()
    			checkSelf(self, 'isIPv6')
    			return data.rawIP:isIPv6()
    		end
    
    		function obj:containsIP(ip)
    			checkSelf(self, 'containsIP')
    			local tp = type(ip)
    			if tp == 'string' then
    				ip = makeIPAddress(ip)
    			elseif tp == 'table' then
    				validateIPAddress('containsIP', 1, ip)
    			else
    				checkTypeMulti('containsIP', 1, ip, {'string', 'table'})
    			end
    			if self:getVersion() == ip:getVersion() then
    				return self:getPrefix() <= ip and ip <= self:getHighestIP()
    			end
    			return false
    		end
    
    		function obj:overlapsCollection(collection)
    			checkSelf(self, 'overlapsCollection')
    			validateCollection('overlapsCollection', 1, collection)
    			return collection:overlapsSubnet(self)
    		end
    
    		function obj:overlapsSubnet(subnet)
    			checkSelf(self, 'overlapsSubnet')
    			local tp = type(subnet)
    			if tp == 'string' then
    				subnet = makeSubnet(subnet)
    			elseif tp == 'table' then
    				validateSubnet('overlapsSubnet', 1, subnet)
    			else
    				checkTypeMulti('overlapsSubnet', 1, subnet, {'string', 'table'})
    			end
    			if self:getVersion() == subnet:getVersion() then
    				return (
    					subnet:getHighestIP() >= self:getPrefix() and
    					subnet:getPrefix() <= self:getHighestIP()
    				)
    			end
    			return false
    		end
    
    		function obj:walk()
    			checkSelf(self, 'walk')
    			local started
    			local current = self:getPrefix()
    			local highest = self:getHighestIP()
    			return function ()
    				if not started then
    					started = true
    					return current
    				end
    				if current < highest then
    					current = current:getNextIP()
    					return current
    				end
    			end
    		end
    
    		return obj
    	end
    
    	makeSubnet = function (cidr)
    		-- Return a Subnet object from a CIDR string. If the CIDR string is
    		-- invalid, throw an error.
    		local lhs, rhs = cidr: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 and (n == 0 or not rhs:find('^0')) then
    				-- The right-hand side is a number between 0 and 32 (for IPv4)
    				-- or 0 and 128 (for IPv6) and doesn't have any leading zeroes.
    				local base = RawIP.newFromIP(lhs)
    				if base then
    					-- The left-hand side is a valid IP address.
    					local prefix = base:getPrefix(n)
    					if base == prefix then
    						-- The left-hand side is the lowest IP in the subnet.
    						return makeSubnetFromRaw(prefix, n)
    					end
    				end
    			end
    		end
    		error(string.format("'%s' is an invalid CIDR string", cidr), 3)
    	end
    
    	function Subnet.new(cidr)
    		checkType('Subnet.new', 1, cidr, 'string')
    		return makeSubnet(cidr)
    	end
    end
    
    --------------------------------------------------------------------------------
    -- Ranges class
    -- Holds a list of IPAdress pairs representing contiguous IP ranges.
    --------------------------------------------------------------------------------
    
    local Ranges = Collection.new()
    Ranges.__index = Ranges
    
    function Ranges.new()
    	return setmetatable({}, Ranges)
    end
    
    function Ranges:add(ip1, ip2)
    	validateIPAddress('add', 1, ip1)
    	if ip2 ~= nil then
    		validateIPAddress('add', 2, ip2)
    		if ip1 > ip2 then
    			error('The first IP must be less than or equal to the second', 2)
    		end
    	end
    	Collection.add(self, {ip1, ip2 or ip1})
    end
    
    function Ranges:merge()
    	self:sort(
    		function (lhs, rhs)
    			-- Sort by second value, then first.
    			if lhs[2] == rhs[2] then
    				return lhs[1] < rhs[1]
    			end
    			return lhs[2] < rhs[2]
    		end
    	)
    	local pos = self.n
    	while pos > 1 do
    		for i = pos - 1, 1, -1 do
    			local ip1 = self[i][2]
    			local ip2 = ip1:getNextIP()
    			if ip2 < ip1 then
    				ip2 = ip1  -- don't wrap around
    			end
    			if self[pos][1] > ip2 then
    				break
    			end
    			ip1 = self[i][1]
    			ip2 = self[pos][1]
    			self[i] = {ip1 > ip2 and ip2 or ip1, self[pos][2]}
    			self:remove(pos)
    			pos = pos - 1
    			if pos <= 1 then
    				break
    			end
    		end
    		pos = pos - 1
    	end
    end
    
    --------------------------------------------------------------------------------
    -- IPCollection class
    -- Holds a list of IP addresses/subnets. Used internally.
    -- Each address/subnet has the same version (either IPv4 or IPv6).
    --------------------------------------------------------------------------------
    
    local IPCollection = {}
    IPCollection.__index = IPCollection
    
    function IPCollection.new(version)
    	assert(
    		version == V4 or version == V6,
    		'IPCollection.new called with an invalid version'
    	)
    	local obj = {
    		version = version,               -- V4 or V6
    		addresses = Collection.new(),    -- valid IP addresses
    		subnets = Collection.new(),      -- valid subnets
    		omitted = Collection.new(),      -- not-quite valid strings
    	}
    	return obj
    end
    
    function IPCollection:getVersion()
    	-- Return a string with the IP version of addresses in this collection.
    	return self.version
    end
    
    function IPCollection:_store(hit, stripColons)
    	local maker, location
    	if hit:find('/', 1, true) then
    		maker = Subnet.new
    		location = self.subnets
    	else
    		maker = IPAddress.new
    		location = self.addresses
    	end
    	local success, obj = pcall(maker, hit)
    	if success then
    		location:add(obj)
    	else
    		if stripColons then
    			local colons, hit = hit:match('^(:*)(.*)')
    			if colons ~= '' then
    				self:_store(hit)
    				return
    			end
    		end
    		self.omitted:add(hit)
    	end
    end
    
    function IPCollection:_assertVersion(version, msg)
    	if self.version ~= version then
    		error(msg, 3)
    	end
    end
    
    function IPCollection:addIP(ip)
    	local tp = type(ip)
    	if tp == 'string' then
    		ip = makeIPAddress(ip)
    	elseif tp == 'table' then
    		validateIPAddress('addIP', 1, ip)
    	else
    		checkTypeMulti('addIP', 1, ip, {'string', 'table'})
    	end
    	self:_assertVersion(ip:getVersion(), 'addIP called with incorrect IP version')
    	self.addresses:add(ip)
    	return self
    end
    
    function IPCollection:addSubnet(subnet)
    	local tp = type(subnet)
    	if tp == 'string' then
    		subnet = makeSubnet(subnet)
    	elseif tp == 'table' then
    		validateSubnet('addSubnet', 1, subnet)
    	else
    		checkTypeMulti('addSubnet', 1, subnet, {'string', 'table'})
    	end
    	self:_assertVersion(subnet:getVersion(), 'addSubnet called with incorrect subnet version')
    	self.subnets:add(subnet)
    	return self
    end
    
    function IPCollection:containsIP(ip)
    	-- Return true, obj if ip is in this collection,
    	-- where obj is the first IPAddress or Subnet with the ip.
    	-- Otherwise, return false.
    	local tp = type(ip)
    	if tp == 'string' then
    		ip = makeIPAddress(ip)
    	elseif tp == 'table' then
    		validateIPAddress('containsIP', 1, ip)
    	else
    		checkTypeMulti('containsIP', 1, ip, {'string', 'table'})
    	end
    	if self:getVersion() == ip:getVersion() then
    		for _, item in ipairs(self.addresses) do
    			if item == ip then
    				return true, item
    			end
    		end
    		for _, item in ipairs(self.subnets) do
    			if item:containsIP(ip) then
    				return true, item
    			end
    		end
    	end
    	return false
    end
    
    function IPCollection:getRanges()
    	-- Return a sorted table of IP pairs equivalent to the collection.
    	-- Each IP pair is a table representing a contiguous range of
    	-- IP addresses from pair[1] to pair[2] inclusive (IPAddress objects).
    	local ranges = Ranges.new()
    	for _, item in ipairs(self.addresses) do
    		ranges:add(item)
    	end
    	for _, item in ipairs(self.subnets) do
    		ranges:add(item:getPrefix(), item:getHighestIP())
    	end
    	ranges:merge()
    	ranges:deobjectify()
    	return ranges
    end
    
    function IPCollection:overlapsSubnet(subnet)
    	-- Return true, obj if subnet overlaps this collection,
    	-- where obj is the first IPAddress or Subnet overlapping the subnet.
    	-- Otherwise, return false.
    	local tp = type(subnet)
    	if tp == 'string' then
    		subnet = makeSubnet(subnet)
    	elseif tp == 'table' then
    		validateSubnet('overlapsSubnet', 1, subnet)
    	else
    		checkTypeMulti('overlapsSubnet', 1, subnet, {'string', 'table'})
    	end
    	if self:getVersion() == subnet:getVersion() then
    		for _, item in ipairs(self.addresses) do
    			if subnet:containsIP(item) then
    				return true, item
    			end
    		end
    		for _, item in ipairs(self.subnets) do
    			if subnet:overlapsSubnet(item) then
    				return true, item
    			end
    		end
    	end
    	return false
    end
    
    --------------------------------------------------------------------------------
    -- IPv4Collection class
    -- Holds a list of IPv4 addresses/subnets.
    --------------------------------------------------------------------------------
    
    local IPv4Collection = setmetatable({}, IPCollection)
    IPv4Collection.__index = IPv4Collection
    
    function IPv4Collection.new()
    	return setmetatable(IPCollection.new(V4), IPv4Collection)
    end
    
    function IPv4Collection:addFromString(text)
    	-- Extract any IPv4 addresses or CIDR subnets from given text.
    	checkType('addFromString', 1, text, 'string')
    	text = text:gsub('[:!"#&\'()+,%-;<=>?[%]_{|}]', ' ')
    	for hit in text:gmatch('%S+') do
    		if hit:match('^%d+%.%d+[%.%d/]+$') then
    			local _, n = hit:gsub('%.', '.')
    			if n >= 3 then
    				self:_store(hit)
    			end
    		end
    	end
    	return self
    end
    
    --------------------------------------------------------------------------------
    -- IPv6Collection class
    -- Holds a list of IPv6 addresses/subnets.
    --------------------------------------------------------------------------------
    
    local IPv6Collection = setmetatable({}, IPCollection)
    IPv6Collection.__index = IPv6Collection
    
    do
    	-- Private static methods
    	local function isCollectionObject(val)
    		-- Return true if val is probably derived from an IPCollection object,
    		-- otherwise return false.
    		if type(val) == 'table' then
    			local mt = getmetatable(val)
    			if mt == IPv4Collection or mt == IPv6Collection then
    				return true
    			end
    		end
    		return false
    	end
    
    	validateCollection = makeValidationFunction('IPCollection', isCollectionObject)
    
    	function IPv6Collection.new()
    		return setmetatable(IPCollection.new(V6), IPv6Collection)
    	end
    
    	function IPv6Collection:addFromString(text)
    		-- Extract any IPv6 addresses or CIDR subnets from given text.
    		-- Want to accept all valid IPv6 despite the fact that addresses used
    		-- are unlikely to start with ':'.
    		-- Also want to be able to parse arbitrary wikitext which might use
    		-- colons for indenting.
    		-- Therefore, if an address at the start of a line is valid, use it;
    		-- otherwise strip any leading colons and try again.
    		checkType('addFromString', 1, text, 'string')
    		for line in string.gmatch(text .. '\n', '[\t ]*(.-)[\t\r ]*\n') do
    			line = line:gsub('[!"#&\'()+,%-;<=>?[%]_{|}]', ' ')
    			for position, hit in line:gmatch('()(%S+)') do
    				local ip = hit:match('^([:%x]+)/?%d*$')
    				if ip then
    					local _, n = ip:gsub(':', ':')
    					if n >= 2 then
    						self:_store(hit, position == 1)
    					end
    				end
    			end
    		end
    		return self
    	end
    end
    
    return {
    	IPAddress = IPAddress,
    	Subnet = Subnet,
    	IPv4Collection = IPv4Collection,
    	IPv6Collection = IPv6Collection,
    }