Module:IP

-- IP library

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

-- Load modules

require('strict')

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,

}