Module:IP: Difference between revisions

From the Croc Wiki, the Croc encyclopedia
Jump to navigationJump to search
Content added Content deleted
(implement Subnet (very rushed; will check and refactor!))
(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)
 
(45 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
-- TODO Remove items from these notes when satisfied they are ok.
--------------------------------------------------------------------------------


local function makeValidationFunction(className, isObjectFunc)
-- IPAddress does not need mt:__le(obj) as Lua evaluates (a <= b) as (not (b < a))
-- Make a function for validating a specific object.
-- and that is correct for IP addresses.
return function (methodName, argIdx, arg)

if not isObjectFunc(arg) then
-- IPAddress obj:getIP() needs thought.
error(string.format(
-- At first, it looked like the representation of the IP (the numbers).
"bad argument #%d to '%s' (not a valid %s object)",
-- But perhaps it is intended to be a string as in the testcases.
argIdx, methodName, className
-- Is it really just a copy of the input string?
), 3)
-- What if the input is " 1.2.3.4 " (with leading and trailing spaces which
end
-- the code ignores)?
end
-- Also, tostring(ip) for an IPv6 address will return the normalized string
end
-- which might be different from the input. Example:
-- input "1:02:003:0004:0:0:0:0" is "1:2:3:4::" after normalization.


--------------------------------------------------------------------------------
-- Should obj:getIPParts() be removed?
-- RawIP class
-- Instead, move the metamethods into IPAddress.new(ip) so they can
-- Numeric representation of an IPv4 or IPv6 address. Used internally.
-- access the data upvalue?
-- Hmmm, that would work only for self, not for the other object.
-- 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
-- Is there a way of keeping data private but accessing it from metamethods?
-- copying the data to a new table.
-- If getIPParts is kept, perhaps it should be changed to "getParts" or similar
-- as "IP" is redundant in an IPAddress class.

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


local function collection()
local RawIP = {}
RawIP.__index = RawIP
-- Return a table to hold items.
return {
n = 0,
add = function (self, item)
self.n = self.n + 1
self[self.n] = item
end,
join = function (self, sep)
return table.concat(self, sep)
end,
sort = function (self, comp)
table.sort(self, comp)
end,
}
end


do
local function copyChanged(parts, down)
-- Collection class.
-- Return a copy of IPv4 or IPv6 parts, incremented or decremented.
-- This is a table used to hold items.
-- Will wraparound:
local Collection
-- increment 255.255.255.255 → 0.0.0.0
do
-- ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff → ::
local mt = {
-- decrement 0.0.0.0 → 255.255.255.255
__index = {
-- :: → ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
add = function (self, item)
local result = { n = parts.n }
self.n = self.n + 1
local carry = down and 0xffff or 1
self[self.n] = item
for i = parts.n, 1, -1 do
end,
local sum = parts[i] + carry
join = function (self, sep)
if sum >= 0x10000 then
return table.concat(self, sep)
carry = down and 0x10000 or 1
end,
sum = sum - 0x10000
else
}
}
carry = down and 0xffff or 0
Collection = function ()
return setmetatable({n = 0}, mt)
end
end
result[i] = sum
end
end
return result
end


-- Constructors
local function copyPrefix(parts, length)
function RawIP.newFromIPv4(ipStr)
-- Return a copy of IPv4 or IPv6 parts, masked to length.
-- Return a RawIP object if ipStr is a valid IPv4 string. Otherwise,
local result = { n = parts.n }
-- return nil.
for i = 1, parts.n do
-- This representation is for compatibility with IPv6 addresses.
if length > 0 then
local octets = Collection()
if length >= 16 then
local s = ipStr:match('^%s*(.-)%s*$') .. '.'
result[i] = parts[i]
for item in s:gmatch('(.-)%.') do
length = length - 16
octets:add(item)
else
end
result[i] = bit32.band(parts[i],
if octets.n == 4 then
bit32.arshift(0xffff8000, length - 1))
for i, s in ipairs(octets) do
length = 0
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
end
local parts = Collection()
else
result[i] = 0
for i = 1, 3, 2 do
parts:add(octets[i] * 256 + octets[i+1])
end
return setmetatable(parts, RawIP)
end
end
return nil
end
end
return result
end


local function setHostBits(parts, length)
function RawIP.newFromIPv6(ipStr)
-- Return a copy of IPv4 or IPv6 parts, with the least-significant bits
-- Return a RawIP object if ipStr is a valid IPv6 string. Otherwise,
-- return nil.
-- (host bits) set to 1.
ipStr = ipStr:match('^%s*(.-)%s*$')
-- The most-significant length bits identify the network.
local bits = parts.n * 16
local _, n = ipStr:gsub(':', ':')
if n < 7 then
local width
ipStr, n = ipStr:gsub('::', string.rep(':', 9 - n))
if length <= 0 then
end
width = bits
local parts = Collection()
elseif length >= bits then
for item in (ipStr .. ':'):gmatch('(.-):') do
width = 0
parts:add(item)
else
end
width = bits - length
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
end

local result = { n = parts.n }
function RawIP.newFromIP(ipStr)
for i = parts.n, 1, -1 do
-- Return a new RawIP object from either an IPv4 string or an IPv6
if width > 0 then
-- string. If ipStr is not a valid IPv4 or IPv6 string, then return
if width >= 16 then
-- nil.
result[i] = 0xffff
return RawIP.newFromIPv4(ipStr) or RawIP.newFromIPv6(ipStr)
width = width - 16
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
else
result[i] = bit32.replace(parts[i], 0xffff, width - 1, width)
carry = previous and 0xffff or 0
width = 0
end
end
result[i] = sum
else
result[i] = parts[i]
end
end
return setmetatable(result, RawIP)
end
end
return result
end


local function ipv6String(ip)
function RawIP:getPrefix(bitLength)
-- Return a string equivalent to the given IPv6 address.
-- Return a RawIP object for the prefix of the current IP Address with a
-- bit length of bitLength.
local z1, z2 -- indices of run of zeros to be displayed as "::"
local zstart, zcount
local result = Collection()
result.n = self.n
for i = 1, 9 do
for i = 1, self.n do
-- Find left-most occurrence of longest run of two or more zeros.
if i < 9 and ip[i] == 0 then
if bitLength > 0 then
if zstart then
if bitLength >= 16 then
zcount = zcount + 1
result[i] = self[i]
bitLength = bitLength - 16
else
result[i] = bit32.replace(self[i], 0, 0, 16 - bitLength)
bitLength = 0
end
else
else
zstart = i
result[i] = 0
zcount = 1
end
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
else
width = bits - bitLength
if zcount and zcount > 1 then
end
if not z1 or zcount > z2 - z1 + 1 then
local result = Collection()
z1 = zstart
result.n = self.n
z2 = zstart + zcount - 1
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
end
else
result[i] = self[i]
end
end
zstart = nil
zcount = nil
end
end
return setmetatable(result, RawIP)
end
end

local parts = collection()
function RawIP:_makeIPv6String()
for i = 1, 8 do
-- Return an IPv6 string representation of the object. Behavior is
if z1 and z1 <= i and i <= z2 then
-- undefined if the current object is IPv4.
if i == z1 then
local z1, z2 -- indices of run of zeros to be displayed as "::"
if z1 == 1 or z2 == 8 then
local zstart, zcount
if z1 == 1 and z2 == 8 then
for i = 1, 9 do
return '::'
-- Find left-most occurrence of longest run of two or more zeros.
end
if i < 9 and self[i] == 0 then
parts:add(':')
if zstart then
zcount = zcount + 1
else
else
parts:add('')
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
zstart = nil
zcount = nil
end
end
else
parts:add(string.format('%x', ip[i]))
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 table.concat(parts, ':')
end


local function ipString(ip)
function RawIP:_makeIPv4String()
-- Return a string equivalent to given IP address (IPv4 or IPv6).
-- Return an IPv4 string representation of the object. Behavior is
-- undefined if the current object is IPv6.
if ip.n == 2 then
local parts = Collection()
-- IPv4.
local parts = {}
for i = 1, 2 do
for i = 1, 2 do
local w = ip[i]
local w = self[i]
parts:add(math.floor(w / 256))
local q = i == 1 and 1 or 3
parts[q] = math.floor(w / 256)
parts:add(w % 256)
parts[q+1] = w % 256
end
end
return table.concat(parts, '.')
return parts:join('.')
end
end
return ipv6String(ip)
end


local function ipv4Address(data, ipStr)
function RawIP:__tostring()
-- Return a string equivalent to given IP address (IPv4 or IPv6).
-- If ipStr is a valid IPv4 string, store its parts in data and
if self.n == 2 then
-- return true. Otherwise, return false.
return self:_makeIPv4String()
-- This representation is for compatibility with IPv6 addresses.
else
local octets = collection()
return self:_makeIPv6String()
local s = ipStr:match('^%s*(.-)%s*$') .. '.'
end
for item in s:gmatch('(.-)%.') do
octets:add(item)
end
end

if octets.n == 4 then
function RawIP:__lt(obj)
for i, s in ipairs(octets) do
if s:match('^%d+$') then
if self.n == obj.n then
local num = tonumber(s)
for i = 1, self.n do
if 0 <= num and num <= 255 then
if self[i] ~= obj[i] then
return self[i] < obj[i]
if num > 0 and s:match('^0') then
-- A redundant leading zero is for an IP in octal.
return false
end
octets[i] = num
else
return false
end
end
else
return false
end
end
return false
end
end
return self.n < obj.n
data.parts = collection()
for i = 1, 3, 2 do
data.parts:add(octets[i] * 256 + octets[i+1])
end
return true
end
end
return false
end


local function ipv6Address(data, ipStr)
function RawIP:__eq(obj)
if self.n == obj.n then
-- If ipStr is a valid IPv6 string, store its parts in data and
for i = 1, self.n do
-- return true. Otherwise, return false.
if self[i] ~= obj[i] then
ipStr = ipStr:match('^%s*(.-)%s*$')
local _, n = ipStr:gsub(':', ':')
if n < 7 then
ipStr, n = ipStr:gsub('::', string.rep(':', 9 - n))
end
local parts = collection()
for item in (ipStr .. ':'):gmatch('(.-):') do
parts:add(item)
end
if parts.n == 8 then
for i, s in ipairs(parts) do
if s == '' then
parts[i] = 0
else
local num = tonumber('0x' .. s)
if num and 0 <= num and num <= 65535 then
parts[i] = num
else
return false
return false
end
end
end
end
return true
end
end
return false
data.parts = parts
return true
end
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


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Line 263: Line 345:


do
do
-- dataKey is a unique key to access objects' internal data. This is needed
local uniqueId = {}
-- to access the RawIP objects contained in other IPAddress objects so that
-- Initialize metatable
-- they can be compared with the current object's RawIP object. This data
local mt = {}
-- is not available to other classes or other modules.
local dataKey = {}


-- Private static methods
-- Constructor
function IPAddress.new(ip, parts)
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
-- Set up structure
local obj = setmetatable({}, mt)
local obj = {}
local data = {}
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
-- 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 IPAddress.new(uniqueId, setHostBits(data.parts, bitLength))
end

function obj:getPrefix(bitLength)
return IPAddress.new(uniqueId, 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 IPAddress.new(uniqueId, copyChanged(data.parts))
return makeIPAddressFromRaw(data.rawIP:getAdjacent())
end
end


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


-- Metamethods
-- This is ugly but can't see how else to do it.
return setmetatable(obj, {
function obj:getIPParts()
__eq = ipEquals,
return data.parts
__lt = ipLessThan,
end
__concat = concatIP,

__tostring = ipToString,
-- Set initial values
__index = function (self, key)
if ip == uniqueId then
-- If any code knows the unique data key, allow it to access
data.parts = parts
-- the data table.
else
if key == dataKey then
checkType('IPAddress.new', 1, ip, 'string')
return data
if not (ipv4Address(data, ip) or ipv6Address(data, ip)) then
error('invalid IP', 2)
end
end
data.version = data.parts.n == 2 and V4 or V6

return obj
end

-- Metamethods
function mt:__concat(obj)
return tostring(self) .. tostring(obj)
end

function mt:__eq(obj)
local lhs, rhs = self:getIPParts(), obj:getIPParts()
if lhs.n == rhs.n then
for i = 1, lhs.n do
if lhs[i] ~= rhs[i] then
return false
end
end
end
end,
__metatable = false, -- Don't allow access to the metatable
return true
end
})
return false
end
end


function mt:__lt(obj)
makeIPAddress = function (ip)
local lhs, rhs = self:getIPParts(), obj:getIPParts()
local rawIP = RawIP.newFromIP(ip)
if lhs.n == rhs.n then
if not rawIP then
error(string.format("'%s' is an invalid IP address", ip), 3)
for i = 1, lhs.n do
if lhs[i] ~= rhs[i] then
return lhs[i] < rhs[i]
end
end
return false
end
end
return lhs.n < rhs.n
return makeIPAddressFromRaw(rawIP)
end
end


function mt:__tostring()
function IPAddress.new(ip)
checkType('IPAddress.new', 1, ip, 'string')
return ipString(self:getIPParts())
return makeIPAddress(ip)
end
end
end

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


Line 402: 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 449: 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 458: Line 618:
end
end


function obj:walk()
-- Set initial values
checkSelf(self, 'walk')
checkType('Subnet.new', 1, cidr, 'string')
local started
if not makeSubnet(data, cidr) then
local current = self:getPrefix()
error('invalid CIDR', 2)
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
end
data.version = data.parts.n == 2 and V4 or V6


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