Module:IP: Difference between revisions

From the Croc Wiki, the Croc encyclopedia
Jump to navigationJump to search
Content added Content deleted
(a subnet has getPrefix and so does not need its own copy of parts)
(need to use __index to have a unique key which is really private - if outside code gets uniqueKey with getmetatable then they can set it in another table's __eq metamethod, thus foiling isSubnetObject)
 
(34 intermediate revisions by 2 users not shown)
Line 3: Line 3:


-- Load modules
-- Load modules
require('Module:No globals')
local bit32 = require('bit32')
local bit32 = require('bit32')
local libraryUtil = require('libraryUtil')
local libraryUtil = require('libraryUtil')
local checkType = libraryUtil.checkType
local checkType = libraryUtil.checkType
local checkTypeMulti = libraryUtil.checkTypeMulti
local makeCheckSelfFunction = libraryUtil.makeCheckSelfFunction


-- Constants
-- Constants
Line 12: Line 15:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Helper functions
-- Collection class
-- A table to hold items. Used internally.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


local function makeValidationFunction(className, isObjectFunc)
local Collection
-- Make a function for validating a specific object.
do
return function (methodName, argIdx, arg)
local mt = {
if not isObjectFunc(arg) then
add = function (self, item)
error(string.format(
self.n = self.n + 1
"bad argument #%d to '%s' (not a valid %s object)",
self[self.n] = item
argIdx, methodName, className
end,
), 3)
join = function (self, sep)
end
return table.concat(self, sep)
end,
}
Collection = function ()
return setmetatable({n = 0}, mt)
end
end
end
end
Line 43: Line 41:
RawIP.__index = RawIP
RawIP.__index = RawIP


do
function RawIP.newFromIPv4(ipStr)
-- Collection class.
-- If ipStr is a valid IPv4 string, return a collection of its parts.
-- This is a table used to hold items.
-- Otherwise, return nil.
local Collection
-- This representation is for compatibility with IPv6 addresses.
do
local octets = Collection()
local mt = {
local s = ipStr:match('^%s*(.-)%s*$') .. '.'
__index = {
for item in s:gmatch('(.-)%.') do
octets:add(item)
add = function (self, item)
self.n = self.n + 1
self[self.n] = item
end,
join = function (self, sep)
return table.concat(self, sep)
end,
}
}
Collection = function ()
return setmetatable({n = 0}, mt)
end
end
end

if octets.n == 4 then
-- Constructors
for i, s in ipairs(octets) do
function RawIP.newFromIPv4(ipStr)
if s:match('^%d+$') then
-- Return a RawIP object if ipStr is a valid IPv4 string. Otherwise,
local num = tonumber(s)
-- return nil.
if 0 <= num and num <= 255 then
-- This representation is for compatibility with IPv6 addresses.
if num > 0 and s:match('^0') then
local octets = Collection()
-- A redundant leading zero is for an IP in octal.
local s = ipStr:match('^%s*(.-)%s*$') .. '.'
return false
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
end
octets[i] = num
else
else
return false
return nil
end
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 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, n = ipStr:gsub('::', string.rep(':', 9 - n))
end
end
local parts = Collection()
local parts = Collection()
for i = 1, 3, 2 do
for item in (ipStr .. ':'):gmatch('(.-):') do
parts:add(octets[i] * 256 + octets[i+1])
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(s, 16)
if num and 0 <= num and num <= 65535 then
parts[i] = num
else
return nil
end
end
end
return setmetatable(parts, RawIP)
end
end
return setmetatable(parts, RawIP)
return nil
end
end
return nil
end


function RawIP.newFromIPv6(ipStr)
function RawIP.newFromIP(ipStr)
-- Return a new RawIP object from either an IPv4 string or an IPv6
-- If ipStr is a valid IPv6 string, return a collection of its parts.
-- string. If ipStr is not a valid IPv4 or IPv6 string, then return
-- Otherwise, return nil.
-- nil.
ipStr = ipStr:match('^%s*(.-)%s*$')
return RawIP.newFromIPv4(ipStr) or RawIP.newFromIPv6(ipStr)
local _, n = ipStr:gsub(':', ':')
if n < 7 then
ipStr, n = ipStr:gsub('::', string.rep(':', 9 - n))
end
end

local parts = Collection()
-- Methods
for item in (ipStr .. ':'):gmatch('(.-):') do
function RawIP:getVersion()
parts:add(item)
-- Return a string with the version of the IP protocol we are using.
return self.n == 2 and V4 or V6
end
end

if parts.n == 8 then
function RawIP:isIPv4()
for i, s in ipairs(parts) do
-- Return true if this is an IPv4 representation, and false otherwise.
if s == '' then
parts[i] = 0
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: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()
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
else
local num = tonumber('0x' .. s)
carry = previous and 0xffff or 0
end
if num and 0 <= num and num <= 65535 then
parts[i] = num
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()
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
else
result[i] = bit32.replace(self[i], 0, 0, 16 - bitLength)
return false
bitLength = 0
end
end
else
result[i] = 0
end
end
end
end
return setmetatable(parts, RawIP)
return setmetatable(result, RawIP)
end
end
return nil
end


function RawIP:copyChanged(down)
function RawIP:getHighestHost(bitLength)
-- Return a copy of IPv4 or IPv6 parts, incremented or decremented.
-- Return a RawIP object for the highest IP with the prefix of length
-- bitLength. In other words, the network (the most-significant bits)
-- Will wraparound:
-- is the same as the current IP's, but the host bits (the
-- increment 255.255.255.255 → 0.0.0.0
-- least-significant bits) are all set to 1.
-- ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff → ::
local bits = self.n * 16
-- decrement 0.0.0.0 → 255.255.255.255
local width
-- :: → ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
if bitLength <= 0 then
local result = Collection()
width = bits
result.n = self.n
elseif bitLength >= bits then
local carry = down and 0xffff or 1
width = 0
for i = self.n, 1, -1 do
local sum = self[i] + carry
if sum >= 0x10000 then
carry = down and 0x10000 or 1
sum = sum - 0x10000
else
else
carry = down and 0xffff or 0
width = bits - bitLength
end
end
result[i] = sum
local result = Collection()
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
end
return setmetatable(result, RawIP)
end


function RawIP:copyPrefix(length)
function RawIP:_makeIPv6String()
-- Return a copy of IPv4 or IPv6 parts, masked to length.
-- Return an IPv6 string representation of the object. Behavior is
-- undefined if the current object is IPv4.
local result = Collection()
local z1, z2 -- indices of run of zeros to be displayed as "::"
result.n = self.n
local zstart, zcount
for i = 1, self.n do
if length > 0 then
for i = 1, 9 do
-- Find left-most occurrence of longest run of two or more zeros.
if length >= 16 then
result[i] = self[i]
if i < 9 and self[i] == 0 then
length = length - 16
if zstart then
zcount = zcount + 1
else
zstart = i
zcount = 1
end
else
else
if zcount and zcount > 1 then
result[i] = bit32.band(self[i],
if not z1 or zcount > z2 - z1 + 1 then
bit32.arshift(0xffff8000, length - 1))
length = 0
z1 = zstart
z2 = zstart + zcount - 1
end
end
zstart = nil
zcount = nil
end
end
else
result[i] = 0
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', self[i]))
end
end
return parts:join(':')
end
end
return setmetatable(result, RawIP)
end


function RawIP:setHostBits(length)
function RawIP:_makeIPv4String()
-- Return a copy of IPv4 or IPv6 parts, with the least-significant bits
-- Return an IPv4 string representation of the object. Behavior is
-- undefined if the current object is IPv6.
-- (host bits) set to 1.
local parts = Collection()
-- The most-significant length bits identify the network.
local bits = self.n * 16
for i = 1, 2 do
local width
local w = self[i]
parts:add(math.floor(w / 256))
if length <= 0 then
parts:add(w % 256)
width = bits
end
elseif length >= bits then
return parts:join('.')
width = 0
else
width = bits - length
end
end

local result = Collection()
function RawIP:__tostring()
result.n = self.n
-- Return a string equivalent to given IP address (IPv4 or IPv6).
for i = self.n, 1, -1 do
if width > 0 then
if self.n == 2 then
return self:_makeIPv4String()
if width >= 16 then
result[i] = 0xffff
width = width - 16
else
result[i] = bit32.replace(self[i], 0xffff, width - 1, width)
width = 0
end
else
else
result[i] = self[i]
return self:_makeIPv6String()
end
end
end
end
return setmetatable(result, rawIP)
end


function RawIP:_makeIPv6String()
function RawIP:__lt(obj)
if self.n == obj.n then
-- Return an IPv6 string representation of the object. Behavior is undefined
for i = 1, self.n do
-- if the current object is IPv4.
if self[i] ~= obj[i] then
local z1, z2 -- indices of run of zeros to be displayed as "::"
return self[i] < obj[i]
local zstart, zcount
for i = 1, 9 do
-- Find left-most occurrence of longest run of two or more zeros.
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
end
end
zstart = nil
return false
zcount = nil
end
end
return self.n < obj.n
end
end

local parts = Collection()
function RawIP:__eq(obj)
for i = 1, 8 do
if z1 and z1 <= i and i <= z2 then
if self.n == obj.n then
if i == z1 then
for i = 1, self.n do
if z1 == 1 or z2 == 8 then
if self[i] ~= obj[i] then
return false
if z1 == 1 and z2 == 8 then
return '::'
end
parts:add(':')
else
parts:add('')
end
end
end
end
return true
else
parts:add(string.format('%x', self[i]))
end
end
return false
end
end
return parts:join(':')
end
end


--------------------------------------------------------------------------------
function RawIP:_makeIPv4String()
-- Initialize private methods available to IPAddress and Subnet
-- Return an IPv4 string representation of the object. Behavior is undefined
--------------------------------------------------------------------------------
-- if the current object is IPv6.
local parts = Collection()
for i = 1, 2 do
local w = self[i]
parts:add(math.floor(w / 256))
parts:add(w % 256)
end
return parts:join('.')
end


-- Both IPAddress and Subnet need access to each others' private constructor
function RawIP:__tostring()
-- functions. IPAddress must be able to make Subnet objects from CIDR strings
-- Return a string equivalent to given IP address (IPv4 or IPv6).
-- and from RawIP objects, and Subnet must be able to make IPAddress objects
if self.n == 2 then
-- from IP strings and from RawIP objects. These constructors must all be
return self:_makeIPv4String()
-- private to ensure correct error levels and to stop other modules from having
else
-- to worry about RawIP objects. Because they are private, they must be
return self:_makeIPv6String()
-- initialized here.
end
local makeIPAddress, makeIPAddressFromRaw, makeSubnet, makeSubnetFromRaw
end

-- IPAddress and Subnet also need to be able to validate Subnet and IPAddress
-- objects that they are passed as input, so initialize those functions here
-- as well.
local validateIPAddress, validateSubnet


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Line 255: Line 343:


local IPAddress = {}
local IPAddress = {}

-- newIPFromParts constructs a new IPAddress object from its parts. It needs to
-- be accessible from the Subnet object but it should not be public.
local newIPFromParts


do
do
local dataKey = {} -- A unique key to access objects' internal data.
-- 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
-- Metamethods that don't need upvalues
local function ipEquals(ip1, ip2)
local function ipEquals(ip1, ip2)
local lhs = ip1[dataKey].parts
return ip1[dataKey].rawIP == ip2[dataKey].rawIP
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
end


local function ipLessThan(ip1, ip2)
local function ipLessThan(ip1, ip2)
local lhs = ip1[dataKey].parts
return ip1[dataKey].rawIP < ip2[dataKey].rawIP
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
end


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


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


-- Constructors
-- Constructors
newIPFromParts = function (parts)
makeIPAddressFromRaw = function (rawIP)
-- Constructs a new IPAddress object from its parts. This function is
-- Constructs a new IPAddress object from a rawIP object. This function
-- for internal use; it is called by IPAddress.new and can be called
-- is for internal use; it is called by IPAddress.new and from other
-- from the Subnet class, but it is not available to other modules.
-- IPAddress methods, and should be available to the Subnet class, but
-- should not be available to other modules.
assert(type(parts) == 'table', 'parts was type ' .. type(parts) .. '; expected type table')
assert(type(rawIP) == 'table', 'rawIP was type ' .. type(rawIP) .. '; expected type table')


-- Set up structure
-- Set up structure
local obj = {}
local obj = {}
local data = {
local data = {}
parts = parts,
data.rawIP = rawIP

version = parts.n == 2 and V4 or V6,
-- A function to check whether methods are called with a valid self
}
-- parameter.
local checkSelf = makeCheckSelfFunction(
'IP',
'ipAddress',
obj,
'IPAddress object'
)


-- Public methods
-- Public methods
function obj:getIP()
function obj:getIP()
checkSelf(self, 'getIP')
return ipString(data.parts)
return tostring(data.rawIP)
end
end


function obj:getVersion()
function obj:getVersion()
checkSelf(self, 'getVersion')
return data.version
return data.rawIP:getVersion()
end

function obj:getHighestIP(bitLength)
return newIPFromParts(setHostBits(data.parts, bitLength))
end

function obj:getPrefix(bitLength)
return newIPFromParts(copyPrefix(data.parts, bitLength))
end
end


function obj:isIPv4()
function obj:isIPv4()
checkSelf(self, 'isIPv4')
return data.version == V4
return data.rawIP:isIPv4()
end
end


function obj:isIPv6()
function obj:isIPv6()
checkSelf(self, 'isIPv6')
return data.version == V6
return data.rawIP:isIPv6()
end
end


function obj:isInSubnet(subnet)
function obj:isInSubnet(subnet)
checkSelf(self, 'isInSubnet')
-- TODO Consider alternative of checking:
local tp = type(subnet)
-- (ipFirst <= self and self <= ipLast)
if self:getVersion() == subnet:getVersion() then
if tp == 'string' then
local prefix = self:getPrefix(subnet:getBitLength())
subnet = makeSubnet(subnet)
return prefix == subnet:getPrefix()
elseif tp == 'table' then
validateSubnet('isInSubnet', 1, subnet)
else
checkTypeMulti('isInSubnet', 1, subnet, {'string', 'table'})
end
end
return false
return subnet:containsIP(self)
end

function obj:getSubnet(bitLength)
checkSelf(self, 'getSubnet')
checkType('getSubnet', 1, bitLength, 'number')
return makeSubnetFromRaw(data.rawIP, bitLength)
end
end


function obj:getNextIP()
function obj:getNextIP()
checkSelf(self, 'getNextIP')
return newIPFromParts(copyChanged(data.parts))
return makeIPAddressFromRaw(data.rawIP:getAdjacent())
end
end


function obj:getPreviousIP()
function obj:getPreviousIP()
checkSelf(self, 'getPreviousIP')
return newIPFromParts(copyChanged(data.parts, true))
return makeIPAddressFromRaw(data.rawIP:getAdjacent(true))
end
end


Line 361: Line 451:
__eq = ipEquals,
__eq = ipEquals,
__lt = ipLessThan,
__lt = ipLessThan,
__concat = concatIPs,
__concat = concatIP,
__tostring = ipToString,
__tostring = ipToString,
__index = function (self, key)
__index = function (self, key)
-- If any code knows the unique data key, allow it to access
-- the data table.
if key == dataKey then
if key == dataKey then
return data
return data
end
end
end,
end,
__metatable = false, -- Don't allow access to the metatable
})
})
end
end


function IPAddress.new(ip)
makeIPAddress = function (ip)
local rawIP = RawIP.newFromIP(ip)
checkType('IPAddress.new', 1, ip, 'string')
if not rawIP then
local parts = parseIPv4(ip) or parseIPv6(ip)
error(string.format("'%s' is an invalid IP address", ip), 3)
if not parts then
error('invalid IP', 2)
end
end
return newIPFromParts(parts)
return makeIPAddressFromRaw(rawIP)
end
end
end


local function makeSubnet(data, cidrStr)
function IPAddress.new(ip)
checkType('IPAddress.new', 1, ip, 'string')
-- If cidrStr is a valid IPv4 or IPv6 CIDR specification, store its
return makeIPAddress(ip)
-- information in data and return its version. Otherwise, return nil.
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.bitLength = n
data.prefix = prefix
data.highestIP = base:getHighestIP(n)
return bits == 32 and V4 or V6
end
end
end
end
return nil
end
end


Line 410: Line 486:


do
do
-- uniqueKey is a unique, private key used to test whether a given object
-- Initialize metatable
-- is a Subnet object.
local mt = {}
local uniqueKey = setmetatable({}, {__metatable = false})


-- Private static methods
-- Constructor
function Subnet.new(cidr)
local function isSubnetObject(val)
-- Return true if val is a Subnet object, and false otherwise.
return getmetatable(val) == uniqueKey
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)

-- Metamethods that don't need upvalues
local function subnetEquals(subnet1, subnet2)
return subnet1:getCIDR() == subnet2:getCIDR()
end

local function concatenateSubnets(subnet1, subnet2)
return tostring(subnet1) .. tostring(subnet2)
end

local function subnetToString(subnet)
return subnet:getCIDR()
end

-- Constructors
makeSubnetFromRaw = function (rawIP, bitLength)
-- Set up structure
-- Set up structure
local obj = setmetatable({}, mt)
local obj = {}
local data = {}
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
-- Public methods
function obj:getPrefix()
function obj:getPrefix()
checkSelf(self, 'getPrefix')
if not data.prefix then
data.prefix = makeIPAddressFromRaw(
data.rawIP:getPrefix(data.bitLength)
)
end
return data.prefix
return data.prefix
end
end


function obj:getHighestIP()
function obj:getHighestIP()
checkSelf(self, 'getHighestIP')
if not data.highestIP then
data.highestIP = makeIPAddressFromRaw(
data.rawIP:getHighestHost(data.bitLength)
)
end
return data.highestIP
return data.highestIP
end
end


function obj:getBitLength()
function obj:getBitLength()
checkSelf(self, 'getBitLength')
return data.bitLength
return data.bitLength
end
end


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


function obj:getVersion()
function obj:getVersion()
checkSelf(self, 'getVersion')
return data.version
return data.rawIP:getVersion()
end
end


function obj:isIPv4()
function obj:isIPv4()
checkSelf(self, 'isIPv4')
return data.version == V4
return data.rawIP:isIPv4()
end
end


function obj:isIPv6()
function obj:isIPv6()
checkSelf(self, 'isIPv6')
return data.version == V6
return data.rawIP:isIPv6()
end
end


function obj:containsIP(ip)
function obj:containsIP(ip)
checkSelf(self, 'containsIP')
-- TODO See ip:isInSubnet(subnet); use this technique there?
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
if self:getVersion() == ip:getVersion() then
return self:getPrefix() <= ip and ip <= self:getHighestIP()
return self:getPrefix() <= ip and ip <= self:getHighestIP()
Line 457: Line 600:


function obj:overlapsSubnet(subnet)
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
if self:getVersion() == subnet:getVersion() then
return (
return (
Line 466: Line 618:
end
end


function obj:walk()
-- Set initial values
checkSelf(self, 'walk')
checkType('Subnet.new', 1, cidr, 'string')
local started
data.version = makeSubnet(data, cidr)
local current = self:getPrefix()
if not data.version then
local highest = self:getHighestIP()
error('invalid CIDR', 2)
return function ()
if not started then
started = true
return current
end
if current < highest then
current = current:getNextIP()
return current
end
end
end
end


return obj
return setmetatable(obj, {
__index = function (self, key)
if key == uniqueKey then
return true
end
end,
__eq = subnetEquals,
__concat = concatenateSubnets,
__tostring = subnetToString,
__metatable = false,
})
end
end


makeSubnet = function (cidr)
-- Metamethods
-- Return a Subnet object from a CIDR string. If the CIDR string is
function mt:__eq(obj)
-- invalid, throw an error.
return self:getCIDR() == obj:getCIDR()
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
end


function mt:__tostring()
function Subnet.new(cidr)
checkType('Subnet.new', 1, cidr, 'string')
return self:getCIDR()
return makeSubnet(cidr)
end
end
end
end

Latest revision as of 04:15, July 23, 2016

Module:IP is a library for working with IP addresses and subnets. It can handle both IPv4 and IPv6. The library exports four classes, IPAddress, Subnet, IPv4Collection, and IPv6Collection.

Loading the library

local IP = require('Module:IP')
local IPAddress = IP.IPAddress
local Subnet = IP.Subnet

IPAddress

The IPAddress class is used to work with single IP addresses. To create a new IPAddress object:

local ipAddress = IPAddress.new(ipString)

The ipString variable can be a valid IPv4 or IPv6 address.

Examples:

local ipv4Address = IPAddress.new('1.2.3.4')
local ipv6Address = IPAddress.new('2001:db8::ff00:12:3456')

IPAddress objects can be compared with relational operators:

-- Equality
IPAddress.new('1.2.3.4') == IPAddress.new('1.2.3.4') -- true
IPAddress.new('1.2.3.4') == IPAddress.new('1.2.3.5') -- false

-- Less than / greater than
IPAddress.new('1.2.3.4') < IPAddress.new('1.2.3.5')  -- true
IPAddress.new('1.2.3.4') > IPAddress.new('1.2.3.5')  -- false
IPAddress.new('1.2.3.4') <= IPAddress.new('1.2.3.5') -- true
IPAddress.new('1.2.3.4') <= IPAddress.new('1.2.3.4') -- true

You can use tostring on them (this is equivalent to using getIP):

tostring(IPAddress.new('1.2.3.4'))                -- "1.2.3.4"
tostring(IPAddress.new('2001:db8::ff00:12:3456')) -- "2001:db8::ff00:12:3456"

-- Expanded IPv6 addresses are abbreviated:
tostring(IPAddress.new('2001:db8:0:0:0:0:0:0'))   -- "2001:db8::"

You can also concatenate them:

IPAddress.new('1.2.3.4') .. ' foo'                   -- "1.2.3.4 foo"
IPAddress.new('1.2.3.4') .. IPAddress.new('5.6.7.8') -- "1.2.3.45.6.7.8"

IPAddress objects have several methods, outlined below.

getIP

ipAddress:getIP()

Returns a string representation of the IP address. IPv6 addresses are abbreviated if possible.

Examples:

IPAddress.new('1.2.3.4'):getIP()                -- "1.2.3.4"
IPAddress.new('2001:db8::ff00:12:3456'):getIP() -- "2001:db8::ff00:12:3456"
IPAddress.new('2001:db8:0:0:0:0:0:0'):getIP()   -- "2001:db8::"

getVersion

ipAddress:getVersion()

Returns the version of the IP protocol being used. This is "IPv4" for IPv4 addresses, and "IPv6" for IPv6 addresses.

Examples:

IPAddress.new('1.2.3.4'):getVersion()                -- "IPv4"
IPAddress.new('2001:db8::ff00:12:3456'):getVersion() -- "IPv6"

isIPv4

ipAddress:isIPv4()

Returns true if the IP address is an IPv4 address, and false otherwise.

Examples:

IPAddress.new('1.2.3.4'):isIPv4()                -- true
IPAddress.new('2001:db8::ff00:12:3456'):isIPv4() -- false

isIPv6

ipAddress:isIPv6()

Returns true if the IP address is an IPv6 address, and false otherwise.

Examples:

IPAddress.new('1.2.3.4'):isIPv6()                -- false
IPAddress.new('2001:db8::ff00:12:3456'):isIPv6() -- true

isInSubnet

ipAddress:isInSubnet(subnet)

Returns true if the IP address is in the subnet subnet, and false otherwise. subnet may be a Subnet object or a CIDR string.

Examples:

IPAddress.new('1.2.3.4'):isInSubnet('1.2.3.0/24')                             -- true
IPAddress.new('1.2.3.4'):isInSubnet('1.2.4.0/24')                             -- false
IPAddress.new('1.2.3.4'):isInSubnet(Subnet.new('1.2.3.0/24'))                 -- true
IPAddress.new('2001:db8::ff00:12:3456'):isInSubnet('2001:db8::ff00:12:0/112') -- true

getSubnet

ipAddress:getSubnet(bitLength)

Returns a Subnet object for the subnet with a bit length of bitLength which contains the current IP. The bitLength parameter must be an integer between 0 and 32 for IPv4 addresses, or an integer between 0 and 128 for IPv6 addresses.

Examples:

IPAddress.new('1.2.3.4'):getSubnet(24) -- Equivalent to Subnet.new('1.2.3.0/24')

getNextIP

ipAddress:getNextIP()

Returns a new IPAddress object equivalent to the current IP address incremented by one. The IPv4 address "255.255.255.255" rolls around to "0.0.0.0", and the IPv6 address "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" rolls around to "::".

Examples:

IPAddress.new('1.2.3.4'):getNextIP()                -- Equivalent to IPAddress.new('1.2.3.5')
IPAddress.new('2001:db8::ff00:12:3456'):getNextIP() -- Equivalent to IPAddress.new('2001:db8::ff00:12:3457')
IPAddress.new('255.255.255.255'):getNextIP()        -- Equivalent to IPAddress.new('0.0.0.0')

getPreviousIP

ipAddress:getPreviousIP()

Returns a new IPAddress object equivalent to the current IP address decremented by one. The IPv4 address "0.0.0.0" rolls around to "255.255.255.255", and the IPv6 address "::" rolls around to "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff".

Examples:

IPAddress.new('1.2.3.4'):getPreviousIP()                -- Equivalent to IPAddress.new('1.2.3.3')
IPAddress.new('2001:db8::ff00:12:3456'):getPreviousIP() -- Equivalent to IPAddress.new('2001:db8::ff00:12:3455')
IPAddress.new('0.0.0.0'):getPreviousIP()                -- Equivalent to IPAddress.new('255.255.255.255')

Subnet

The Subnet class is used to work with subnetworks of IPv4 or IPv6 addresses. To create a new Subnet object:

local subnet = Subnet.new(cidrString)

cidrString must be a valid IPv4 or IPv6 CIDR string.

Subnet objects can be compared for equality:

Subnet.new('1.2.3.0/24') == Subnet.new('1.2.3.0/24')                           -- true
Subnet.new('1.2.3.0/24') == Subnet.new('1.2.3.0/25')                           -- false
Subnet.new('1.2.3.0/24') == Subnet.new('2001:db8::ff00:12:0/112')              -- false
Subnet.new('2001:db8::ff00:12:0/112') == Subnet.new('2001:db8::ff00:12:0/112') -- true
Subnet.new('2001:db8:0:0:0:0:0:0/112') == Subnet.new('2001:db8::/112')         -- true

You can use tostring on them (this is equivalent to getCIDR):

tostring(Subnet.new('1.2.3.0/24'))               -- "1.2.3.0/24"
tostring(Subnet.new('2001:db8::ff00:12:0/112'))  -- "2001:db8::ff00:12:0/112"
tostring(Subnet.new('2001:db8:0:0:0:0:0:0/112')) -- "2001:db8::/112"

You can also concatenate them:

Subnet.new('1.2.3.0/24') .. ' foo'                   -- "1.2.3.0/24 foo"
Subnet.new('1.2.3.0/24') .. Subnet.new('4.5.6.0/24') -- "1.2.3.0/244.5.6.0/24"

Subnet objects have several methods, outlined below.

getPrefix

subnet:getPrefix()

Returns an IPAddress object for the lowest IP address in the subnet.

Examples:

Subnet.new('1.2.3.0/24'):getPrefix()              -- Equivalent to IPAddress.new('1.2.3.0')
Subnet.new('2001:db8::ff00:12:0/112'):getPrefix() -- Equivalent to IPAddress.new('2001:db8::ff00:12:0')

getHighestIP

subnet:getHighestIP()

Returns an IPAddress object for the highest IP address in the subnet.

Examples:

Subnet.new('1.2.3.0/24'):getHighestIP()              -- Equivalent to IPAddress.new('1.2.3.255')
Subnet.new('2001:db8::ff00:12:0/112'):getHighestIP() -- Equivalent to IPAddress.new('2001:db8::ff00:12:ffff')

getBitLength

subnet:getBitLength()

Returns the bit length of the subnet. This is an integer between 0 and 32 for IPv4 addresses, or an integer between 0 and 128 for IPv6 addresses.

Examples:

Subnet.new('1.2.3.0/24'):getBitLength()              -- 24
Subnet.new('2001:db8::ff00:12:0/112'):getBitLength() -- 112

getCIDR

subnet:getCIDR()

Returns a CIDR string representation of the subnet.

Examples:

Subnet.new('1.2.3.0/24'):getCIDR()               -- "1.2.3.0/24"
Subnet.new('2001:db8::ff00:12:0/112'):getCIDR()  -- "2001:db8::ff00:12:0/112"
Subnet.new('2001:db8:0:0:0:0:0:0/112'):getCIDR() -- "2001:db8::/112"

getVersion

subnet:getVersion()

Returns the version of the IP protocol being used. This is "IPv4" for IPv4 addresses, and "IPv6" for IPv6 addresses.

Examples:

Subnet.new('1.2.3.0/24'):getVersion()              -- "IPv4"
Subnet.new('2001:db8::ff00:12:0/112'):getVersion() -- "IPv6"

isIPv4

subnet:isIPv4()

Returns true if the subnet is using IPv4, and false otherwise.

Examples:

Subnet.new('1.2.3.0/24'):isIPv4()              -- true
Subnet.new('2001:db8::ff00:12:0/112'):isIPv4() -- false

isIPv6

subnet:isIPv6()

Returns true if the subnet is using IPv6, and false otherwise.

Examples:

Subnet.new('1.2.3.0/24'):isIPv6()              -- false
Subnet.new('2001:db8::ff00:12:0/112'):isIPv6() -- true

containsIP

subnet:containsIP(ip)

Returns true if the subnet contains the IP address ip, and false otherwise. ip can be an IP address string, or an IPAddress object.

Examples:

Subnet.new('1.2.3.0/24'):containsIP('1.2.3.4')                             -- true
Subnet.new('1.2.3.0/24'):containsIP('1.2.4.4')                             -- false
Subnet.new('1.2.3.0/24'):containsIP(IPAddress.new('1.2.3.4'))              -- true
Subnet.new('2001:db8::ff00:12:0/112'):containsIP('2001:db8::ff00:12:3456') -- true

overlapsSubnet

subnet:overlapsSubnet(subnet)

Returns true if the current subnet overlaps with subnet, and false otherwise. subnet can be a CIDR string or a subnet object.

Examples:

Subnet.new('1.2.3.0/24'):overlapsSubnet('1.2.0.0/16')                         -- true
Subnet.new('1.2.3.0/24'):overlapsSubnet('1.2.12.0/22')                        -- false
Subnet.new('1.2.3.0/24'):overlapsSubnet(Subnet.new('1.2.0.0/16'))             -- true
Subnet.new('2001:db8::ff00:12:0/112'):overlapsSubnet('2001:db8::ff00:0:0/96') -- true

walk

subnet:walk()

The walk method iterates over all of the IPAddress objects in the subnet.

Examples:

for ipAddress in Subnet.new('192.168.0.0/30'):walk() do
	mw.log(tostring(ipAddress))
end
-- 192.168.0.0
-- 192.168.0.1
-- 192.168.0.2
-- 192.168.0.3

IPv4Collection

The IPv4Collection class is used to work with several different IPv4 addresses and IPv4 subnets. To create a new IPv4Collection object:

local collection = IPv4Collection.new()

IPv4Collection objects have several methods, outlined below.

getVersion

collection:getVersion()

Returns the string "IPv4".

addIP

collection:addIP(ip)

Adds an IP to the collection. The IP can be either a string or an IPAddress object.

Examples:

collection:addIP('1.2.3.4')
collection:addIP(IPAddress.new('1.2.3.4'))

This method is chainable:

collection:addIP('1.2.3.4'):addIP('5.6.7.8')

addSubnet

collection:addSubnet(subnet)

Adds a subnet to the collection. The subnet can be either a CIDR string or a Subnet object.

Examples:

collection:addSubnet('1.2.3.0/24')
collection:addSubnet(Subnet.new('1.2.3.0/24'))

This method is chainable:

collection:addSubnet('1.2.0.0/24'):addSubnet('1.2.1.0/24')

addFromString

collection:addFromString(str)

Extracts any IPv4 addresses and IPv4 CIDR subnets from str and adds them to the collection. Any text that is not an IPv4 address or CIDR subnet is ignored.

Examples:

collection:addFromString('Add some IPs and subnets: 1.2.3.4 1.2.3.5 2001:0::f foo 1.2.4.0/24')

This method is chainable:

collection:addFromString('foo 1.2.3.4'):addFromString('bar 5.6.7.8')

containsIP

collection:containsIP(ip)

Returns true if the collection contains the specified IP; otherwise returns false. The ip parameter can be a string or an IPAddress object.

Examples:

collection:containsIP('1.2.3.4')
collection:containsIP(IPAddress.new('1.2.3.4'))

getRanges

collection:getRanges()

Returns a sorted array of IP pairs equivalent to the collection. Each IP pair is an array representing a contiguous range of IP addresses from pair[1] to pair[2] inclusive. pair[1] and pair[2] are IPAddress objects.

Examples:

collection:addSubnet('1.2.0.0/24')
collection:addSubnet('1.2.1.0/24')
collection:addSubnet('1.2.10.0/24')
mw.logObject(collection:getRanges())
-- Logs the following:
-- table#1 {
--   table#2 {
--     1.2.0.0,
--     1.2.1.255,
--   },
--   table#3 {
--     1.2.10.0,
--     1.2.10.255,
--   },
-- }

overlapsSubnet

collection:overlapsSubnet(subnet)

Returns true, obj if subnet overlaps this collection, where obj is the first IPAddress or Subnet object overlapping the subnet. Otherwise, returns false. subnet can be a CIDR string or a Subnet object.

Examples:

collection:addIP('1.2.3.4')
collection:overlapsSubnet('1.2.3.0/24') -- true, IPAddress.new('1.2.3.4')
collection:overlapsSubnet('1.2.4.0/24') -- false

IPv6Collection

The IPv6Collection class is used to work with several different IPv6 addresses and IPv6 subnets. IPv6Collection objects are directly analogous to IPv4Collection objects: they contain the same methods and work the same way, but all IP addresses and subnets added to it must be IPv6, not IPv4.

To create a new IPv6Collection object:

local collection = IPv6Collection.new()



-- 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

--------------------------------------------------------------------------------
-- 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

do
	-- Collection class.
	-- This is a table used to hold items.
	local Collection
	do
		local mt = {
			__index = {
				add = function (self, item)
					self.n = self.n + 1
					self[self.n] = item
				end,
				join = function (self, sep)
					return table.concat(self, sep)
				end,
			}
		}
		Collection = function ()
			return setmetatable({n = 0}, mt)
		end
	end

	-- 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()
		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()
			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, 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(s, 16)
					if num and 0 <= num and num <= 65535 then
						parts[i] = num
					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: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()
		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()
		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()
		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 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 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()
		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()
		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
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

-- IPAddress and Subnet also need to be able to validate Subnet and IPAddress
-- objects that they are passed as input, so initialize those functions here
-- as well.
local 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: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')
			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 = setmetatable({}, {__metatable = false})

	-- Private static methods
	local function isSubnetObject(val)
		-- Return true if val is a Subnet object, and false otherwise.
		return getmetatable(val) == uniqueKey
	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)

	-- Metamethods that don't need upvalues
	local function subnetEquals(subnet1, subnet2)
		return subnet1:getCIDR() == subnet2:getCIDR()
	end

	local function concatenateSubnets(subnet1, subnet2)
		return tostring(subnet1) .. tostring(subnet2)
	end

	local function subnetToString(subnet)
		return subnet:getCIDR()
	end

	-- Constructors
	makeSubnetFromRaw = function (rawIP, bitLength)
		-- Set up structure
		local obj = {}
		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: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 setmetatable(obj, {
			__index = function (self, key)
				if key == uniqueKey then
					return true
				end
			end,
			__eq = subnetEquals,
			__concat = concatenateSubnets,
			__tostring = subnetToString,
			__metatable = false,
		})
	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

return {
	IPAddress = IPAddress,
	Subnet = Subnet,
}